Repository navigation
fix(commerce-server): return 400/404 from order-status instead of 500 - #1488
anam-godaddy wants to merge 7 commits into
Conversation
Invalid order IDs (blank, padded, `.`, `..`) and missing orders previously surfaced as 500s, indistinguishable from credential or upstream failures. - getOrderStatus throws InvalidOrderIdError for invalid IDs and OrderNotFoundError when the Orders API returns 404 or the order is bound to another order ID, store, or channel. Incomplete responses stay generic. - The order-status route maps these to 400 and 404; everything else is 500. - Padded IDs are rejected rather than trimmed so the lookup never targets a different ID than the caller supplied. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: c43083d The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
The 500 body echoed the internal error message, which can name upstream
HTTP statuses, OAuth/token failures, or configuration problems. Return only
{ success: false, error } and log the detail with console.error.
Export ORDER_STATUS_UNKNOWN so callers compare against a constant rather than
the 'unknown' literal; status stays a string because the Orders API's full
paymentStatus set is not documented here.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
I found one error-mapping issue worth fixing. No P0/P1 blockers found.
Checks performed on commit
|
…lures An Orders API response whose context lacks storeId or channelId fell through to the store/channel mismatch check and was reported as 404. Require both binding fields before comparing them so malformed responses keep the generic 500 and server-side logging. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks, good catch. Fixed in b238a33.
Coverage added:
Typecheck and Biome pass; 180 tests pass. |
pbennett1-godaddy
left a comment
There was a problem hiding this comment.
Review notes inline. The most important are the bare-404 classification and the changeset bump level. The broader message-leak issue (other routes still return error.message in 500 bodies) looks like it's addressed by #1494.
| cache: 'no-store', | ||
| }, | ||
| ); | ||
| if (response.status === 404) throw new OrderNotFoundError(); |
There was a problem hiding this comment.
Any upstream 404 becomes OrderNotFoundError, including a 404 caused by a wrong storeId in the path or an apiBaseUrl that doesn't serve the Orders route. Every lookup would then report "Order not found" with nothing logged (the 404 branch skips console.error), so a misconfiguration looks like missing orders. Can we tell a real missing-order 404 apart (e.g. by body shape or error code) and treat anything else as an upstream failure?
Also: the response body isn't consumed or cancelled on this path or the !response.ok path. Under undici, unconsumed bodies can hold pooled connections while the order status is being polled. await response.body?.cancel() before throwing would avoid that.
There was a problem hiding this comment.
Partly addressed in 456728b.
The body is now cancelled before throwing on every non-OK response, including 404. Tests assert ReadableStream.cancel is called for 404, 401, 403, and 500.
I left the 404 classification as is. The Orders API doesn't document an error code or body shape that separates a missing order from a wrong store ID or base URL, and guessing could turn real missing orders into 500s. Instead, upstream 404s are now logged with console.warn (including the configured store ID), so a misconfiguration shows up in logs instead of passing silently. If there's a documented not-found code we can key on, I'm happy to check for it.
There was a problem hiding this comment.
Follow-up in c43083d (supersedes my reply above): the 404 is now classified by the Orders API's error body, after checking order-api's REST handler.
- A missing order (or one in another store) is 404
{ code: 'NOT_FOUND' }from its error handler. Only that maps toOrderNotFoundError. - Any other 404 (Express's HTML "Cannot GET" for an unrouted path, or a different base URL's 404) is now an upstream failure: logged 500. The
console.warnis removed. - 422
{ code: 'VALIDATION_FAILED' }, which order-api returns when it can't decode the order ID, maps toInvalidOrderIdError(400) instead of 500. - A wrong
storeIddoesn't produce a 404 at all: order-api's authorization check returns 403, which already goes down the logged 500 path.
The 404/422 body is read to check code; other error bodies are still cancelled.
| throw new Error('Order lookup returned a different store or channel'); | ||
| const context = order?.context; | ||
| // A binding missing either field is a malformed upstream response, not proof of another store or channel. | ||
| if (!order?.id || !isBindingId(context?.storeId) || !isBindingId(context?.channelId)) { |
There was a problem hiding this comment.
The store and channel IDs are type-checked with isBindingId, but order.id is only checked for truthiness. A non-string id (e.g. a number) passes here, then fails order.id !== orderId below and is reported as 404 instead of a malformed-response 500. Suggest !isBindingId(order?.id) for consistency.
There was a problem hiding this comment.
Done in 456728b. order.id now goes through isBindingId, so a missing or non-string ID throws the generic error and becomes a 500. Both cases are in the incomplete-response it.each.
| throw new Error('Order lookup did not return the requested order'); | ||
| } | ||
| // Report an order bound to another store or channel as missing so the route doesn't reveal it exists. | ||
| if (order.id !== orderId || context.storeId !== storeId || context.channelId !== channelId) { |
There was a problem hiding this comment.
Only the store/channel mismatch needs the existence-hiding 404. An order.id !== orderId mismatch means upstream returned the wrong record or a canonicalized ID, which is a malformed response. As written, a real (possibly paid) order is shown as not found with nothing logged. Could the id mismatch throw the generic error (500 + log) instead?
There was a problem hiding this comment.
Agreed, done in 456728b. An order.id mismatch now throws the generic malformed-response error (500 + console.error). Only a store/channel mismatch returns 404. Added helper and route tests for the ID mismatch and removed it from the not-found cases.
| message: error instanceof Error ? error.message : String(error), | ||
| }); | ||
| if (error instanceof InvalidOrderIdError) { | ||
| res.status(400).json(invalidOrderIdBody); |
There was a problem hiding this comment.
instanceof breaks if a host ends up with two copies of the package (nested node_modules, or one bundled and one external copy). A not-found order would then return 500. The constructors already set name, so checking error.name (or a brand symbol) would hold up better.
There was a problem hiding this comment.
Done in 456728b. The route now matches by error.name instead of instanceof. I added a route test that throws a plain Error with each name, standing in for a different copy of the package, and asserts 400/404. The README and changeset tell in-process callers to check error.name too. I didn't add exported guard functions for now.
| export default async function handler(req: Request, res: Response): Promise<void> { | ||
| try { | ||
| const { orderId } = req.query; | ||
| if (!orderId || typeof orderId !== 'string') { |
There was a problem hiding this comment.
nit: getOrderStatus already validates the ID and throws InvalidOrderIdError, so there are two 400 paths to keep in sync. Only the non-string (array query value) case needs handling here. Consider narrowing to typeof orderId !== 'string' and delegating the rest.
There was a problem hiding this comment.
Done in 456728b. The route only rejects non-string query values now; getOrderStatus handles blank, padded, . and ... The existing 400 cases still pass with no upstream call.
| @@ -0,0 +1,5 @@ | |||
| --- | |||
| '@godaddy/gd-commerce-server': patch | |||
There was a problem hiding this comment.
This adds three public exports and removes message from the 500 response body, which hosts may be reading. That seems like at least minor rather than patch.
There was a problem hiding this comment.
Agreed, bumped to minor in 456728b (the package is 0.x, so that's the breaking bump).
| const result = await requestOrderStatus(`?orderId=${order.id}`); | ||
| expect(result).toEqual({ status: 500, body: { success: false, error: 'Failed to get order status' } }); | ||
| expect(log).toHaveBeenCalledWith('order-status: failed to get order status', expect.any(Error)); | ||
| log.mockRestore(); |
There was a problem hiding this comment.
nit: if an assertion above fails, mockRestore() never runs and console.error stays mocked for the rest of the file. Move it to afterEach or a try/finally.
There was a problem hiding this comment.
Done in 456728b. vi.restoreAllMocks() now runs in the top-level afterEach, and the inline mockRestore() is gone.
- Treat an upstream order ID mismatch or non-string ID as a malformed response (500 + log) instead of not found. - Cancel unread error response bodies and log upstream 404s. - Match route errors by name so they survive duplicate package copies. - Leave string ID validation to getOrderStatus. - Restore mocks in afterEach. - Bump the changeset to minor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ror code The Orders API reports a missing order as 404 with code NOT_FOUND and an undecodable order ID as 422 with code VALIDATION_FAILED. Only those map to OrderNotFoundError and InvalidOrderIdError; any other 404, such as an unrouted path or misconfigured base URL, is an upstream failure (logged 500). This replaces the console.warn on every upstream 404. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rror-bodies Brings in godaddy#1488's review fixes and main. Conflicts resolved toward this branch's error types: - get-order-status: godaddy#1488's classification (Orders API NOT_FOUND -> 404, VALIDATION_FAILED -> 400, incomplete/mismatched order or any other 404 -> malformed) now throws UpstreamError (502) instead of a plain Error. - order-status GET: keeps commerceRoute, with godaddy#1488's narrowed `typeof orderId !== 'string'` check. The name-based error check and its "another copy" test are dropped; the route and lib share one module. - README and tests: godaddy#1488's cases expressed as 502/`code` bodies. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What changed
GET /api/commerce/order-statusreturned 500 for every failure, so a bad order ID or a missing order looked the same as a credential or upstream outage. Its 500 body also echoed the internal error message..or..orderId{ code: 'VALIDATION_FAILED' }){ code: 'NOT_FOUND' })message{ success: false, error: 'Failed to get order status' }; detail logged withconsole.errorgetOrderStatus()throwsInvalidOrderIdError/OrderNotFoundErrorso in-process callers can branch without parsing messages. Checkerror.namerather thaninstanceof, which fails when a host has more than one copy of this package; the route itself matches by name. The 404 body is generic and never includes upstream content.ORDER_STATUS_UNKNOWN('unknown') is exported so callers compare against a constant.statusstaysstring: the Orders API's fullpaymentStatusset isn't documented here, and a closed union would mistype unlisted values.' abc'passed validation, was sent upstream untrimmed, and failed the binding check as a 500. Documented in the README and covered by tests.Status semantics
statusis the Orders REST API'sstatuses.paymentStatus(e.g.PAID,PENDING), orORDER_STATUS_UNKNOWNwhen absent. This has been the behaviour since 5314b51 moved order lookup from the storefront GraphQL subgraph (which only returns draft orders and has no status) to REST; this PR doesn't change it. The README now also states that the status is display enrichment and callers must never present an unconfirmed payment as paid.Why
Callers couldn't tell a bad or missing order ID from a credential or upstream outage, so they couldn't show the right message or alert on the right failure. The changeset is
minor: this adds public exports and removesmessagefrom the 500 body, which hosts may read. On0.xthat is the breaking bump, so consumers on@godaddy/gd-commerce-server@^0.1.1need their range bumped to pick it up.Only a 404 with the Orders API's own
NOT_FOUNDcode is mapped to "not found". From order-api's REST handler:{ code: 'NOT_FOUND' }from its error handler (dataloaders/orders.ts,rest/middlewares/errorHandler.ts). Completed orders are returned, so a paid order is a 200, never a 404.code; a wrong base URL or gateway returns its own 404. These are misconfiguration, so they stay a logged 500 instead of every lookup reporting "Order not found".storeIdfails order-api's authorization check with 403, not 404, and was already a logged 500.Prefix_part) is 422{ code: 'VALIDATION_FAILED' }, reported as 400. This package doesn't enforce the prefix itself.Out of scope
messagein their 500 bodies. feat(commerce-server): standardize route error handling #1494 standardizes error handling across all routes, including this one, and builds on this PR.draftOrder { statuses }on the checkout subgraph.Testing
pnpm --filter @godaddy/gd-commerce-server typecheck,lint,test— 193 passed.,.., repeated param — no upstream call; Orders APIVALIDATION_FAILED), 404 (Orders APINOT_FOUND, store mismatch), and 500 (denied token, upstream 500, HTML 404 from an unrouted path, incomplete order, missing store binding, different order ID) asserting the exact generic body and that the detail is loggedcode, another code, non-JSON) are upstream failures; unread error bodies are cancelled; a plainErrornamedInvalidOrderIdError/OrderNotFoundErrorstill maps to 400 / 404 at the route🤖 Generated with Claude Code