8000 fix: 운영 중계에서 세븐일레븐 실패 진단 전달 by hmmhmmhm · Pull Request #245 · hmmhmmhm/daiso-mcp · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/convenience-relay.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,5 @@ CU 매장 키워드만 입력한 조회는 공식 웹에서 해당 지역 점포
원본 `/real-stock/multi/01/stocks`에서 매장 1곳은 성공했으나 2곳은 HTTP 400, 코드 501, `정상적인 점포 조회 요청이 아닙니다.`로 거부되는 현상을 확인했습니다. 영구 정책인지 일시 제한인지는 확인되지 않았습니다.

이 응답은 공개 API에서 HTTP 502를 유지하고, 오류 문구를 `정상적인 점포 조회 요청이 아닙니다. 일시적인 외부 서비스 오류입니다. 잠시 후 재시도해주세요.`로 표시합니다. API와 MCP의 공통 `diagnostics`에는 원본 `upstreamStatus: 400`, `upstreamCode: 501`, `upstreamMessage`가 남습니다. 확인된 문구만 전달하며 응답 본문 전체나 알 수 없는 오류 문구는 공개하지 않습니다. 이 요청 거부는 `retryable: false`로 표시해 자동 재시도를 하지 않습니다. 안내 문구는 정책이 일시적이라는 확정 진단이 아닙니다.

중계 내부에서는 이 확인된 거부를 HTTP 200의 `success: false` 실패 envelope로 전달해 중간 연결 구간의 5xx 오류 페이지 처리와 분리합니다. Worker는 이를 성공으로 반환하지 않고 HTTP 502로 변환합니다. 실패는 캐시하지 않으며 원본 호출 로그에도 오류로 남습니다. 구버전 중계의 HTTP 502 진단도 계속 읽을 수 있습니다.
3 changes: 2 additions & 1 deletion scripts/relay/http-relay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,8 @@ export function createHttpRelay(token: string, options: HttpRelayOptions) {
? error.status
: 502;
resultStatus = status;
return reply(status, {
return reply(error instanceof RelayError && error.upstreamError ? 200 : status, {
...(error instanceof RelayError && error.upstreamError ? { success: false } : {}),
error: 'Relay request failed',
upstreamError: error instanceof RelayError ? error.upstreamError : undefined,
});
Expand Down
17 changes: 17 additions & 0 deletions src/utils/convenienceTransport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,23 @@ async function requestConvenienceRelayRaw<T>(
}
readingResponse = true;
const data: unknown = await response.json();
if (operation === 'seven-stock') {
const failure = parseSevenStockFailure(
data && typeof data === 'object'
? (data as Record<string, unknown>).upstreamError
: undefined,
);
if (failure)
throw new ServiceError(
'CONVENIENCE_RELAY_FAILED',
`${failure.message} ${EXTERNAL_SERVICE_RETRY_HINT}`,
502,
false,
failure.status,
undefined,
failure,
);
}
if (
!data ||
typeof data !== 'object' ||
Expand Down
60 changes: 60 additions & 0 deletions tests/api/seven-stock-error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,63 @@ it('원본 400의 오류 형식이 다르면 본문을 전달하지 않는다',
);
expect(await response.json()).toEqual({ error: 'Relay request failed' });
});
it('원본 조회 거부는 HTTP 중계 오류 페이지 대신 실패 envelope로 전달한다', async () => {
const relay = createConvenienceRelay('token', {
takeQuota: async () => true,
fetcher: async () => Response.json({ success: false, message, code: 501 }, { status: 400 }),
});
const response = await relay(
new Request('http://localhost/v1/convenience/seven-stock', {
method: 'POST',
headers: { Authorization: 'Bearer token' },
body: JSON.stringify(body),
}),
);
expect(response.status).toBe(200);
expect(await response.json()).toMatchObject({
success: false,
upstreamError: { status: 400, code: 501, message },
});
});
it.each([null, 1, {}, { upstreamError: { status: 400, code: 501, message: 'private' } }])(
'정상 HTTP에서도 검증되지 않은 진단을 전달하지 않는다 %j',
async (value) => {
vi.stubGlobal('fetch', async () => Response.json(value));
try {
if (!value || typeof value !== 'object')
await expect(
requestConvenienceRelay('seven-stock', body, {
convenienceRelayUrl: 'https://relay.example',
convenienceRelayToken: 'token',
}),
).rejects.toThrow('실패');
else
expect(
await requestConvenienceRelay('seven-stock', body, {
convenienceRelayUrl: 'https://relay.example',
convenienceRelayToken: 'token',
}),
).toEqual(value);
} finally {
vi.unstubAllGlobals();
}
},
);
it('구버전 중계의 HTTP 502 원본 진단도 보존한다', async () => {
vi.stubGlobal('fetch', async () =>
Response.json(
{ error: 'Relay request failed', upstreamError: { status: 400, code: 501, message } },
{ status: 502 },
),
);
try {
await expect(
requestConvenienceRelay('seven-stock', body, {
convenienceRelayUrl: 'https://relay.example',
convenienceRelayToken: 'token',
}),
).rejects.toMatchObject({ upstreamStatus: 400, retryable: false });
} finally {
vi.unstubAllGlobals();
}
});
Loading
0