From 9635c3d029fd382edd97e012e15c0793a06682c3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:14:42 +0000 Subject: [PATCH 1/3] Initial plan From bc8289a7018967df7437f25ee49dc162bcbb880d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:19:09 +0000 Subject: [PATCH 2/3] Refactor duplicated JSON body parse guards into shared helper --- containers/api-proxy/body-transform.js | 31 ++++++----------------- containers/api-proxy/body-utils.js | 25 ++++++++++++++++++ containers/api-proxy/body-utils.test.js | 17 +++++++++++++ containers/api-proxy/model-resolver.js | 11 +++----- containers/api-proxy/providers/copilot.js | 10 +++----- 5 files changed, 56 insertions(+), 38 deletions(-) create mode 100644 containers/api-proxy/body-utils.js create mode 100644 containers/api-proxy/body-utils.test.js diff --git a/containers/api-proxy/body-transform.js b/containers/api-proxy/body-transform.js index b070dfb6d..ccd995b2a 100644 --- a/containers/api-proxy/body-transform.js +++ b/containers/api-proxy/body-transform.js @@ -1,5 +1,7 @@ 'use strict'; +const { parseBodyAsObject } = require('./body-utils'); + /** * Sanitize OpenAI-compatible request history where tool_calls[].type is null. * @@ -10,14 +12,8 @@ * @returns {{ body: Buffer, normalizedCount: number, droppedCount: number }|null} */ function sanitizeNullToolCallTypes(body) { - let parsed; - try { - parsed = JSON.parse(body.toString('utf8')); - } catch { - return null; - } - - if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.messages)) { + const parsed = parseBodyAsObject(body); + if (!parsed || !Array.isArray(parsed.messages)) { return null; } @@ -82,13 +78,8 @@ function sanitizeNullToolCallTypes(body) { * @returns {Buffer|null} */ function injectSteeringMessage(body, provider, message) { - let parsed; - try { - parsed = JSON.parse(body.toString()); - } catch { - return null; - } - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null; + let parsed = parseBodyAsObject(body); + if (!parsed) return null; if (provider === 'anthropic') { if (typeof parsed.system === 'string') { @@ -148,14 +139,8 @@ function injectStreamOptions(body, provider, requestPath = '') { const pathOnly = typeof requestPath === 'string' ? requestPath.split('?')[0] : ''; if (/^\/?(?:v\d+\/)?responses(?:\/|$)/.test(pathOnly)) return null; - let parsed; - try { - parsed = JSON.parse(body.toString('utf8')); - } catch { - return null; - } - - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null; + const parsed = parseBodyAsObject(body); + if (!parsed) return null; if (!parsed.stream) return null; if (parsed.stream_options) return null; diff --git a/containers/api-proxy/body-utils.js b/containers/api-proxy/body-utils.js new file mode 100644 index 000000000..588fe023b --- /dev/null +++ b/containers/api-proxy/body-utils.js @@ -0,0 +1,25 @@ +'use strict'; + +/** + * Parse a request/response body as a plain JSON object. + * + * Returns null on parse error, non-object, or array input. + * + * @param {Buffer|string} body + * @returns {Record|null} + */ +function parseBodyAsObject(body) { + let parsed; + try { + parsed = JSON.parse(body.toString('utf8')); + } catch { + return null; + } + + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null; + return parsed; +} + +module.exports = { + parseBodyAsObject, +}; diff --git a/containers/api-proxy/body-utils.test.js b/containers/api-proxy/body-utils.test.js new file mode 100644 index 000000000..ae7714981 --- /dev/null +++ b/containers/api-proxy/body-utils.test.js @@ -0,0 +1,17 @@ +const { parseBodyAsObject } = require('./body-utils'); + +describe('parseBodyAsObject', () => { + it('parses a JSON object from a buffer', () => { + const parsed = parseBodyAsObject(Buffer.from(JSON.stringify({ model: 'gpt-5.4' }))); + expect(parsed).toEqual({ model: 'gpt-5.4' }); + }); + + it('returns null for invalid JSON', () => { + expect(parseBodyAsObject(Buffer.from('not-json'))).toBeNull(); + }); + + it('returns null for arrays and non-objects', () => { + expect(parseBodyAsObject(Buffer.from(JSON.stringify([])))).toBeNull(); + expect(parseBodyAsObject(Buffer.from(JSON.stringify('text')))).toBeNull(); + }); +}); diff --git a/containers/api-proxy/model-resolver.js b/containers/api-proxy/model-resolver.js index b1913d21a..a56b2cb75 100644 --- a/containers/api-proxy/model-resolver.js +++ b/containers/api-proxy/model-resolver.js @@ -20,6 +20,7 @@ */ const { getTierSortedModels } = require('./model-discovery'); +const { parseBodyAsObject } = require('./body-utils'); const DEFAULT_MODEL_FALLBACK = Object.freeze({ enabled: true, @@ -418,14 +419,8 @@ function rewriteModelInBody(body, provider, aliases, availableModels, modelFallb // Only attempt rewrite for non-empty bodies if (!body || body.length === 0) return null; - let parsed; - try { - parsed = JSON.parse(body.toString('utf8')); - } catch { - return null; // Non-JSON body — skip - } - - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null; + const parsed = parseBodyAsObject(body); + if (!parsed) return null; // Non-JSON body — skip // Determine the requested model. If absent, try the default alias (""). const originalModel = typeof parsed.model === 'string' ? parsed.model : ''; diff --git a/containers/api-proxy/providers/copilot.js b/containers/api-proxy/providers/copilot.js index 89d7c3df9..be526baa1 100644 --- a/containers/api-proxy/providers/copilot.js +++ b/containers/api-proxy/providers/copilot.js @@ -30,6 +30,7 @@ const { composeBodyTransforms, } = require('../proxy-utils'); const { sanitizeNullToolCallTypes } = require('../body-transform'); +const { parseBodyAsObject } = require('../body-utils'); const { URL } = require('url'); /** @@ -151,13 +152,8 @@ function parseByokExtraBodyFields(raw) { function injectByokExtraBodyFields(body, fields) { if (!fields || Object.keys(fields).length === 0) return null; - let parsed; - try { - parsed = JSON.parse(body.toString('utf8')); - } catch { - return null; - } - if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null; + const parsed = parseBodyAsObject(body); + if (!parsed) return null; let changed = false; for (const [field, value] of Object.entries(fields)) { From af8b94b02db32a78507745afaa757348af0c27e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 5 Jun 2026 00:23:13 +0000 Subject: [PATCH 3/3] Fix: add body-utils.js to Dockerfile COPY instruction --- containers/api-proxy/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/containers/api-proxy/Dockerfile b/containers/api-proxy/Dockerfile index 4cb9075a9..ca32a6a89 100644 --- a/containers/api-proxy/Dockerfile +++ b/containers/api-proxy/Dockerfile @@ -24,7 +24,7 @@ COPY server.js logging.js metrics.js rate-limiter.js \ oidc-token-provider-base.js \ github-oidc.js aws-oidc-token-provider.js gcp-oidc-token-provider.js \ anthropic-oidc-token-provider.js \ - oidc-refresh-utils.js body-transform.js rate-limit.js websocket-proxy.js \ + oidc-refresh-utils.js body-transform.js body-utils.js rate-limit.js websocket-proxy.js \ deprecated-header-tracker.js billing-headers.js upstream-response.js \ anthropic-cache.js otel.js ./ COPY guards/ ./guards/