diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index a69d3829..bcd07c5a 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -331,3 +331,51 @@ Validation: Follow-ups: - Optional parity sweep: apply the same explicit catch-path contract pattern to remaining file routes that still rely on implicit promise-chain errors. + +--- + +### CL-009: TASK22224 proxy-route resilience and encoding parity bundle + +date: 2026-03-23 +author: Cline +scope: `pages/api/file/{getbloblistproxy,getrepsblobproxy,getawaitingsubmissionfromblobproxy,createappealcompletemessageproxy_api}.js`, `tests/phase21/file-handler-contract.test.cjs` +type: change +rationale: Continue larger-slice hardening by aligning proxy handlers with explicit async error handling and safer encoded upstream query forwarding for hash-based downstream calls. +impact: Improves proxy reliability and compatibility for encoded query values while preserving existing proxy error contracts and response behavior. +status: completed + +Summary: + +- `getbloblistproxy.js` + - converted `.then/.catch` chain to explicit `try/catch` + - encoded forwarded `container` and `casefolderID` query values + - preserved error contract: `GET_BLOB_LIST_PROXY_FAILED` +- `getrepsblobproxy.js` + - converted `.then/.catch` chain to explicit `try/catch` + - encoded forwarded `container` + - preserved error contract: `GET_REPS_BLOB_PROXY_FAILED` +- `getawaitingsubmissionfromblobproxy.js` + - converted `.then/.catch` chain to explicit `try/catch` + - preserved error contract: `GET_AWAITING_SUBMISSION_PROXY_FAILED` +- `createappealcompletemessageproxy_api.js` + - converted `.then/.catch` chain to explicit `try/catch` + - encoded forwarded `container` and `tempcaseref` + - preserved error contract: `CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED` +- Phase21 tests expanded for proxy paths: + - getbloblistproxy success + dependency failure + - getrepsblobproxy success + - getawaitingsubmissionfromblobproxy dependency failure + - createappealcompletemessageproxy dependency failure + +Validation: + +- `node tests/phase21/file-handler-contract.test.cjs` -> pass (46/46) +- `node tests/phase21/api-contract-slice1.test.cjs` -> pass + - helper: 4/4 + - file-handler: 46/46 + - email-handler: 12/12 + - endpoint-handler: 149/149 + +Follow-ups: + +- Optional next big slice: bring remaining proxy/message routes using raw axios promise chains (`createcaseinvolvement_api.js`, `createrepinvolvement_api.js`, `updatecase_api.js`) onto the same async/await + explicit contract pattern. diff --git a/pages/api/file/createappealcompletemessageproxy_api.js b/pages/api/file/createappealcompletemessageproxy_api.js index b495040c..81d3c443 100644 --- a/pages/api/file/createappealcompletemessageproxy_api.js +++ b/pages/api/file/createappealcompletemessageproxy_api.js @@ -17,8 +17,8 @@ const hasValue = (value) => typeof value === "string" && value.trim().length > 0; ApiProxy.get(async (req, res) => { - var containerName = req.query.container; - var tempCaseRef = req.query.tempcaseref; + const containerName = req.query.container; + const tempCaseRef = req.query.tempcaseref; if (!hasValue(containerName) || !hasValue(tempCaseRef)) { return respondError(res, { @@ -28,30 +28,28 @@ ApiProxy.get(async (req, res) => { }); } - var token = await getToken(); + const token = await getToken(); - var queryUrl = + const queryUrl = "/api/file/createappealcompletemessage_api?container=" + - containerName + + encodeURIComponent(containerName) + "&tempcaseref=" + - tempCaseRef; + encodeURIComponent(tempCaseRef); - return axios - .get( + try { + const { data } = await axios.get( BASE_URL + queryUrl + hashAPIPath(queryUrl), azureHeaders(token.access_token) - ) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED", - message: "Failed to create appeal complete message" - }); + ); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED", + message: "Failed to create appeal complete message" }); + } }); export const config = { diff --git a/pages/api/file/getawaitingsubmissionfromblobproxy.js b/pages/api/file/getawaitingsubmissionfromblobproxy.js index f04a2198..3dcda590 100644 --- a/pages/api/file/getawaitingsubmissionfromblobproxy.js +++ b/pages/api/file/getawaitingsubmissionfromblobproxy.js @@ -11,7 +11,7 @@ const hasValue = (value) => typeof value === "string" && value.trim().length > 0; export default async function ApiProxy(req, res) { - var containerName = req.query.container; + const containerName = req.query.container; if (!hasValue(containerName)) { return respondError(res, { @@ -21,25 +21,23 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); + const token = await getToken(); - var queryUrl = + const queryUrl = "/api/file/getawaitingsubmissionfromblob?container=" + containerName; - return axios - .get( + try { + const { data } = await axios.get( BASE_URL + queryUrl + hashAPIPath(queryUrl), azureHeaders(token.access_token) - ) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "GET_AWAITING_SUBMISSION_PROXY_FAILED", - message: "Failed to fetch awaiting submission blob" - }); + ); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "GET_AWAITING_SUBMISSION_PROXY_FAILED", + message: "Failed to fetch awaiting submission blob" }); + } } diff --git a/pages/api/file/getbloblistproxy.js b/pages/api/file/getbloblistproxy.js index c6771fca..6dd29acf 100644 --- a/pages/api/file/getbloblistproxy.js +++ b/pages/api/file/getbloblistproxy.js @@ -11,8 +11,8 @@ const hasValue = (value) => typeof value === "string" && value.trim().length > 0; export default async function ApiProxy(req, res) { - var containerName = req.query.container; - var casefolderID = req.query.casefolderID; + const containerName = req.query.container; + const casefolderID = req.query.casefolderID; if (!hasValue(containerName) || !hasValue(casefolderID)) { return respondError(res, { @@ -22,28 +22,26 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); + const token = await getToken(); - var queryUrl = + const queryUrl = "/api/file/getbloblist?container=" + - containerName + + encodeURIComponent(containerName) + "&casefolderID=" + - casefolderID; + encodeURIComponent(casefolderID); - return axios - .get( + try { + const { data } = await axios.get( BASE_URL + queryUrl + hashAPIPath(queryUrl), azureHeaders(token.access_token) - ) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "GET_BLOB_LIST_PROXY_FAILED", - message: "Failed to fetch blob list" - }); + ); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "GET_BLOB_LIST_PROXY_FAILED", + message: "Failed to fetch blob list" }); + } } diff --git a/pages/api/file/getrepsblobproxy.js b/pages/api/file/getrepsblobproxy.js index 36383f1a..64a02e54 100644 --- a/pages/api/file/getrepsblobproxy.js +++ b/pages/api/file/getrepsblobproxy.js @@ -11,7 +11,7 @@ const hasValue = (value) => typeof value === "string" && value.trim().length > 0; export default async function ApiProxy(req, res) { - var containerName = req.query.container; + const containerName = req.query.container; if (!hasValue(containerName)) { return respondError(res, { @@ -21,24 +21,23 @@ export default async function ApiProxy(req, res) { }); } - var token = await getToken(); + const token = await getToken(); - var queryUrl = "/api/file/getrepsblob?container=" + containerName; + const queryUrl = + "/api/file/getrepsblob?container=" + encodeURIComponent(containerName); - return axios - .get( + try { + const { data } = await axios.get( BASE_URL + queryUrl + hashAPIPath(queryUrl), azureHeaders(token.access_token) - ) - .then(({ data }) => { - return respondSuccess(res, data); - }) - .catch((error) => { - consoleLogger(error); - return respondError(res, { - status: 400, - code: "GET_REPS_BLOB_PROXY_FAILED", - message: "Failed to fetch representation blobs" - }); + ); + return respondSuccess(res, data); + } catch (error) { + consoleLogger(error); + return respondError(res, { + status: 400, + code: "GET_REPS_BLOB_PROXY_FAILED", + message: "Failed to fetch representation blobs" }); + } } diff --git a/tests/phase21/file-handler-contract.test.cjs b/tests/phase21/file-handler-contract.test.cjs index 26fa896e..6aa84eb9 100644 --- a/tests/phase21/file-handler-contract.test.cjs +++ b/tests/phase21/file-handler-contract.test.cjs @@ -547,6 +547,153 @@ test("createrepcompletemessage handler dependency failure returns CREATE_REP_COM ); }); +test("getbloblistproxy success returns proxied payload", async () => { + const mod = loadModule("pages/api/file/getbloblistproxy.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + azureHeaders: () => ({ headers: { Authorization: "Bearer tok" } }), + hashAPIPath: () => "&hash=abc", + consoleLogger: () => {}, + process: { env: { API_ROOT: "http://example.com" } }, + axios: { + get: async () => ({ data: { value: [{ id: "b1" }] } }) + } + }); + + const req = { + query: { container: "cont/1", casefolderID: "CASE/1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + value: [{ id: "b1" }] + }); +}); + +test("getbloblistproxy dependency failure returns GET_BLOB_LIST_PROXY_FAILED", async () => { + const mod = loadModule("pages/api/file/getbloblistproxy.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + azureHeaders: () => ({ headers: {} }), + hashAPIPath: () => "&hash=abc", + consoleLogger: () => {}, + process: { env: { API_ROOT: "http://example.com" } }, + axios: { + get: async () => { + throw new Error("proxy failed"); + } + } + }); + + const req = { + query: { container: "c1", casefolderID: "f1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "GET_BLOB_LIST_PROXY_FAILED" + ); +}); + +test("getrepsblobproxy success returns proxied payload", async () => { + const mod = loadModule("pages/api/file/getrepsblobproxy.js", { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + azureHeaders: () => ({ headers: {} }), + hashAPIPath: () => "&hash=abc", + consoleLogger: () => {}, + process: { env: { API_ROOT: "http://example.com" } }, + axios: { + get: async () => ({ data: { value: [{ id: "r1" }] } }) + } + }); + + const req = { + query: { container: "c1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 200); + assert.deepStrictEqual(JSON.parse(JSON.stringify(res.state.jsonBody)), { + value: [{ id: "r1" }] + }); +}); + +test("getawaitingsubmissionfromblobproxy dependency failure returns GET_AWAITING_SUBMISSION_PROXY_FAILED", async () => { + const mod = loadModule( + "pages/api/file/getawaitingsubmissionfromblobproxy.js", + { + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + azureHeaders: () => ({ headers: {} }), + hashAPIPath: () => "&hash=abc", + consoleLogger: () => {}, + process: { env: { API_ROOT: "http://example.com" } }, + axios: { + get: async () => { + throw new Error("awaiting failed"); + } + } + } + ); + + const req = { + query: { container: "c1" } + }; + const res = createRes(); + await mod.default(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "GET_AWAITING_SUBMISSION_PROXY_FAILED" + ); +}); + +test("createappealcompletemessageproxy dependency failure returns CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED", async () => { + const mod = loadModule( + "pages/api/file/createappealcompletemessageproxy_api.js", + { + nextConnect: createNextConnectMock(), + middleware: () => {}, + respondError: respondErrorMock, + respondSuccess: respondSuccessMock, + getToken: async () => ({ access_token: "tok" }), + azureHeaders: () => ({ headers: {} }), + hashAPIPath: () => "&hash=abc", + consoleLogger: () => {}, + process: { env: { API_ROOT: "http://example.com" } }, + axios: { + get: async () => { + throw new Error("create appeal proxy failed"); + } + } + } + ); + + const req = { + query: { container: "c1", tempcaseref: "TMP1" } + }; + const res = createRes(); + await mod.default.handler(req, res); + + assert.strictEqual(res.state.statusCode, 400); + assert.strictEqual( + res.state.jsonBody.error.code, + "CREATE_APPEAL_COMPLETE_MESSAGE_PROXY_FAILED" + ); +}); + test("getawaitingsubmissionfromblob handler success returns payload", async () => { const mod = loadModule("pages/api/file/getawaitingsubmissionfromblob.js", { nextConnect: createNextConnectMock(),