forked from boos/BoosAPI
Tighten alias and header edge-cases called out in review
The latest review pass found a few more concrete edge-cases that were still cheap to close: invalid /responses aliases could still rewrite typos, websocket session headers should prefer proxy-safe dashed forms, contributor rendering needed an input guard, and several upstream request/response helpers still had small contract mismatches around Accept/beta handling and synthesized tool ids. These fixes keep the release branch behavior explicit while the PR remains open. Constraint: Keep changes surgical and well-covered so the review cleanup does not destabilize the already-green release branch Rejected: Leave these for post-merge cleanup | they were still active review threads with straightforward fixes Confidence: high Scope-risk: narrow Directive: When alias routes exist only for ergonomics, reject unknown subpaths loudly instead of silently rewriting them Tested: Focused vitest for contributor updater, route aliases, upstream request builder, openai responses response bridge; typecheck; git diff --check Not-tested: Full npm test; the remaining Gemini review thread that likely needs a broader canonical-tool-result contract decision
This commit is contained in:
@@ -4,12 +4,22 @@ import { ensureResponsesWebsocketTransport } from './responsesWebsocket.js';
|
||||
|
||||
function resolveAliasedResponsesDownstreamPath(
|
||||
request: FastifyRequest,
|
||||
): '/v1/responses' | '/v1/responses/compact' {
|
||||
): '/v1/responses' | '/v1/responses/compact' | null {
|
||||
const rawUrl = request.raw.url || request.url || '';
|
||||
const pathname = rawUrl.split('?')[0] || rawUrl;
|
||||
if (pathname === '/responses') return '/v1/responses';
|
||||
return pathname.endsWith('/compact')
|
||||
? '/v1/responses/compact'
|
||||
: '/v1/responses';
|
||||
: null;
|
||||
}
|
||||
|
||||
async function replyUnsupportedAliasedResponsesPath(reply: FastifyReply) {
|
||||
return reply.code(404).send({
|
||||
error: {
|
||||
message: 'Unknown /responses alias path',
|
||||
type: 'invalid_request_error',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function responsesProxyRoute(app: FastifyInstance) {
|
||||
@@ -29,8 +39,13 @@ export async function responsesProxyRoute(app: FastifyInstance) {
|
||||
|
||||
app.post('/responses', async (request: FastifyRequest, reply: FastifyReply) =>
|
||||
handleOpenAiResponsesSurfaceRequest(request, reply, '/v1/responses'));
|
||||
app.post('/responses/*', async (request: FastifyRequest, reply: FastifyReply) =>
|
||||
handleOpenAiResponsesSurfaceRequest(request, reply, resolveAliasedResponsesDownstreamPath(request)));
|
||||
app.post('/responses/*', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const downstreamPath = resolveAliasedResponsesDownstreamPath(request);
|
||||
if (!downstreamPath) {
|
||||
return replyUnsupportedAliasedResponsesPath(reply);
|
||||
}
|
||||
return handleOpenAiResponsesSurfaceRequest(request, reply, downstreamPath);
|
||||
});
|
||||
app.get('/responses', async (_request: FastifyRequest, reply: FastifyReply) =>
|
||||
reply.code(426).send({
|
||||
error: {
|
||||
@@ -38,4 +53,16 @@ export async function responsesProxyRoute(app: FastifyInstance) {
|
||||
type: 'invalid_request_error',
|
||||
},
|
||||
}));
|
||||
app.get('/responses/*', async (request: FastifyRequest, reply: FastifyReply) => {
|
||||
const downstreamPath = resolveAliasedResponsesDownstreamPath(request);
|
||||
if (!downstreamPath) {
|
||||
return replyUnsupportedAliasedResponsesPath(reply);
|
||||
}
|
||||
return reply.code(426).send({
|
||||
error: {
|
||||
message: `WebSocket upgrade required for GET ${downstreamPath}`,
|
||||
type: 'invalid_request_error',
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -554,8 +554,8 @@ async function handleResponsesWebsocketConnection(
|
||||
) {
|
||||
const websocketSessionId = headerValueToTrimmedString(request.headers['session_id'])
|
||||
|| headerValueToTrimmedString(request.headers['session-id'])
|
||||
|| headerValueToTrimmedString(request.headers['conversation_id'])
|
||||
|| headerValueToTrimmedString(request.headers['conversation-id'])
|
||||
|| headerValueToTrimmedString(request.headers['conversation_id'])
|
||||
|| randomUUID();
|
||||
const runtimeSessionKeys = new Set<string>();
|
||||
let lastRequest: Record<string, unknown> | null = null;
|
||||
|
||||
@@ -95,6 +95,39 @@ describe('proxy route aliases', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects unknown /responses alias subpaths instead of silently rewriting them', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
url: '/responses/other',
|
||||
payload: { model: 'gpt-5.2', input: 'hello' },
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(404);
|
||||
expect(handleOpenAiResponsesSurfaceRequestMock).not.toHaveBeenCalled();
|
||||
expect(response.json()).toEqual({
|
||||
error: {
|
||||
message: 'Unknown /responses alias path',
|
||||
type: 'invalid_request_error',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps GET /responses/compact aligned with the websocket upgrade path', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/responses/compact',
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(426);
|
||||
expect(handleOpenAiResponsesSurfaceRequestMock).not.toHaveBeenCalled();
|
||||
expect(response.json()).toEqual({
|
||||
error: {
|
||||
message: 'WebSocket upgrade required for GET /v1/responses/compact',
|
||||
type: 'invalid_request_error',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('registers bare chat completions alias against the openai chat handler', async () => {
|
||||
const response = await app.inject({
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user