Files
KKTsN c9c652eece fix: improve streaming error propagation and forwarded response logging (#5317)
* Fix: Improve streaming error handling and forwarded response logging

* Fix: fix ESLint error  Strings must use singlequote  quotes

* fix: preserve and log forwarded stream errors

* chore: narrow forwarded stream error fix scope

* fix: make forwardFetchResponse awaitable and forward upstream error text

* Restore original happy path handling

* Remove redundant checks in forwardFetchResponse function

* Don't send anything on parsing error end

---------

Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
2026-04-05 23:01:47 +03:00

167 lines
5.3 KiB
JavaScript

import { afterEach, describe, test, expect, jest } from '@jest/globals';
import { once } from 'node:events';
import { PassThrough } from 'node:stream';
import { Response } from 'node-fetch';
import { CHAT_COMPLETION_SOURCES } from '../src/constants';
import { flattenSchema, forwardFetchResponse } from '../src/util';
function createMockExpressResponse() {
const response = new PassThrough();
response.statusCode = 200;
response.statusMessage = '';
return response;
}
async function collectResponseBody(response) {
const chunks = [];
response.on('data', chunk => chunks.push(Buffer.from(chunk)));
await once(response, 'finish');
return Buffer.concat(chunks).toString('utf8');
}
afterEach(() => {
jest.restoreAllMocks();
});
describe('flattenSchema', () => {
test('should return the schema if it is not an object', () => {
const schema = 'it is not an object';
expect(flattenSchema(schema, CHAT_COMPLETION_SOURCES.MAKERSUITE)).toBe(schema);
});
test('should handle schema with $defs and $ref', () => {
const schema = {
$schema: 'http://json-schema.org/draft-07/schema#',
$defs: {
a: { type: 'string' },
b: {
type: 'object',
properties: {
c: { $ref: '#/$defs/a' },
},
},
},
properties: {
d: { $ref: '#/$defs/b' },
},
};
const expected = {
properties: {
d: {
type: 'object',
properties: {
c: { type: 'string' },
},
},
},
};
expect(flattenSchema(schema, CHAT_COMPLETION_SOURCES.MAKERSUITE)).toEqual(expected);
});
test('should filter unsupported properties for Google API schema', () => {
const schema = {
$defs: {
a: {
type: 'string',
default: 'test',
},
},
type: 'object',
properties: {
b: { $ref: '#/$defs/a' },
c: { type: 'number' },
},
additionalProperties: false,
exclusiveMinimum: 0,
propertyNames: {
pattern: '^[A-Za-z_][A-Za-z0-9_]*$',
},
};
const expected = {
type: 'object',
properties: {
b: {
type: 'string',
},
c: { type: 'number' },
},
};
expect(flattenSchema(schema, CHAT_COMPLETION_SOURCES.MAKERSUITE)).toEqual(expected);
});
test('should not filter properties for non-Google API schema', () => {
const schema = {
$defs: {
a: {
type: 'string',
default: 'test',
},
},
type: 'object',
properties: {
b: { $ref: '#/$defs/a' },
c: { type: 'number' },
},
additionalProperties: false,
exclusiveMinimum: 0,
propertyNames: {
pattern: '^[A-Za-z_][A-Za-z0-9_]*$',
},
};
const expected = {
type: 'object',
properties: {
b: {
type: 'string',
default: 'test',
},
c: { type: 'number' },
},
additionalProperties: false,
exclusiveMinimum: 0,
propertyNames: {
pattern: '^[A-Za-z_][A-Za-z0-9_]*$',
},
};
expect(flattenSchema(schema, 'some-other-api')).toEqual(expected);
});
});
describe('forwardFetchResponse', () => {
test('should log JSON error bodies and return the original body for non-2xx streaming responses', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
const body = JSON.stringify({ error: { message: 'Forbidden by upstream policy' }, detail: 'policy_denied' });
const response = createMockExpressResponse();
const bodyPromise = collectResponseBody(response);
await forwardFetchResponse(new Response(body, {
status: 403,
statusText: 'Forbidden',
}), response);
expect(await bodyPromise).toBe(body);
expect(response.statusCode).toBe(403);
expect(warnSpy).toHaveBeenCalledWith(`Streaming request failed with status 403 Forbidden: ${body}`);
});
test('should log plain text error bodies and return the original body for non-2xx streaming responses', async () => {
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
const body = 'Plain text upstream failure';
const response = createMockExpressResponse();
const bodyPromise = collectResponseBody(response);
await forwardFetchResponse(new Response(body, {
status: 502,
statusText: 'Bad Gateway',
}), response);
expect(await bodyPromise).toBe(body);
expect(response.statusCode).toBe(502);
expect(warnSpy).toHaveBeenCalledWith(`Streaming request failed with status 502 Bad Gateway: ${body}`);
});
});