fix(coding): reject mismatched Data Service errors
This commit is contained in:
@@ -56,6 +56,22 @@ const CONTEXT_KEYS = new Set([
|
|||||||
'retry_after_seconds',
|
'retry_after_seconds',
|
||||||
]);
|
]);
|
||||||
const CONTEXT_RESOURCES = new Set(['instances', 'collections', 'documents', 'bytes']);
|
const CONTEXT_RESOURCES = new Set(['instances', 'collections', 'documents', 'bytes']);
|
||||||
|
const ERROR_STATUS_BY_CODE: Readonly<Record<string, number>> = {
|
||||||
|
invalid_cursor: 400,
|
||||||
|
instance_not_found: 404,
|
||||||
|
collection_not_found: 404,
|
||||||
|
document_not_found: 404,
|
||||||
|
cursor_expired: 410,
|
||||||
|
revision_conflict: 409,
|
||||||
|
quota_exceeded: 409,
|
||||||
|
document_too_large: 413,
|
||||||
|
invalid_project_id: 422,
|
||||||
|
invalid_collection_name: 422,
|
||||||
|
invalid_document_id: 422,
|
||||||
|
invalid_document_data: 422,
|
||||||
|
invalid_revision: 422,
|
||||||
|
rate_limited: 429,
|
||||||
|
};
|
||||||
|
|
||||||
type FetchImplementation = typeof fetch;
|
type FetchImplementation = typeof fetch;
|
||||||
type AccessTokenGetter = typeof getValidWorksSquareAccessToken;
|
type AccessTokenGetter = typeof getValidWorksSquareAccessToken;
|
||||||
@@ -376,6 +392,7 @@ function projectError<T>(payload: unknown, response: Response): DataServiceHostR
|
|||||||
const detail = payload.detail;
|
const detail = payload.detail;
|
||||||
const rawCode = boundedString(detail.code, 64);
|
const rawCode = boundedString(detail.code, 64);
|
||||||
if (!rawCode || !KNOWN_ERROR_CODES.has(rawCode)) return invalidResponse();
|
if (!rawCode || !KNOWN_ERROR_CODES.has(rawCode)) return invalidResponse();
|
||||||
|
if (ERROR_STATUS_BY_CODE[rawCode] !== response.status) return invalidResponse();
|
||||||
if (detail.retryable !== undefined && typeof detail.retryable !== 'boolean') return invalidResponse();
|
if (detail.retryable !== undefined && typeof detail.retryable !== 'boolean') return invalidResponse();
|
||||||
if (detail.context !== undefined && !isRecord(detail.context)) return invalidResponse();
|
if (detail.context !== undefined && !isRecord(detail.context)) return invalidResponse();
|
||||||
const code = rawCode;
|
const code = rawCode;
|
||||||
|
|||||||
@@ -202,6 +202,24 @@ describe('DataServiceCloudClient', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('rejects a known error code paired with the wrong HTTP status', async () => {
|
||||||
|
const client = new DataServiceCloudClient({
|
||||||
|
fetchImpl: vi.fn<typeof fetch>().mockResolvedValue(
|
||||||
|
jsonResponse({ detail: { code: 'quota_exceeded' } }, { status: 400 }),
|
||||||
|
),
|
||||||
|
getAccessToken: vi.fn().mockResolvedValue('access-token'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await client.inspect(projectId);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
success: false,
|
||||||
|
status: 502,
|
||||||
|
code: 'upstream_invalid_response',
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('normalizes malformed client errors and all upstream 5xx responses safely', async () => {
|
it('normalizes malformed client errors and all upstream 5xx responses safely', async () => {
|
||||||
const fetchImpl = vi.fn<typeof fetch>()
|
const fetchImpl = vi.fn<typeof fetch>()
|
||||||
.mockResolvedValueOnce(new Response(null, { status: 404 }))
|
.mockResolvedValueOnce(new Response(null, { status: 404 }))
|
||||||
|
|||||||
Reference in New Issue
Block a user