juspay/hyperswitch · error · Error
Webhook failed with error code "${response.body?.error?.code
Error message
Webhook failed with error code "${response.body?.error?.code}" error message "${response.body?.error?.message}" What it means
This error is thrown by a custom Cypress support command that POSTs a simulated webhook delivery (completeUrl, optional headers, optional HMAC-signed body) with failOnStatusCode: false. After the request resolves it logs the x-request-id header and, when response.status !== 200, throws this Error embedding the server's error.code and error.message. So the message is a relay of the receiving webhook endpoint's own rejection, not a client-side network failure (cy.request would fail differently on DNS/connection errors).
Source
Thrown at cypress-tests/cypress/support/commands.js:8258
const headers = {
"Content-Type": contentType,
};
const sendRequest = () =>
cy
.request({
method: "POST",
url: completeUrl,
headers,
body,
failOnStatusCode: false,
})
.then((response) => {
logRequestId(response.headers["x-request-id"]);
if (response.status !== 200) {
throw new Error(
`Webhook failed with error code "${response.body?.error?.code}" error message "${response.body?.error?.message}"`
);
}
return cy.wrap(response);
});
// If signature required
if (webhookConfig.webhookSecret) {
const bodyString = JSON.stringify(webhookBody);
body = bodyString;
return cy
.task("hmac_sha256", {
secret: webhookConfig.webhookSecret,
message: bodyString,
})
.then((signature) => {View on GitHub (pinned to 9b8b89dc37)
Solutions
- Read the embedded error code/message in the thrown string (and the x-request-id logged just before) to identify the server-side rejection reason, and grep server logs by that request id.
- If 401/403 or a signature error: confirm webhookConfig.webhookSecret matches the currently registered secret, and that the HMAC is computed over the exact same string assigned to `body` (the command already does `const bodyString = JSON.stringify(webhookBody); body = bodyString;` — reuse bodyString for signing).
- If 404: verify completeUrl — correct base URL for the environment, correct path, webhook route enabled for the profile/tenant.
- If 400: inspect webhookBody against the endpoint's schema (required fields, types) and fix the payload in the test fixture.
- If 5xx/502/503: retry after confirming service health; this is a server-side failure, not a test bug.
Example fix
// before: signing one serialization, sending another
const signature = crypto.createHmac('sha256', secret).update(JSON.stringify(webhookBody)).digest('hex');
let body = webhookBody;
// after: sign the exact bytes that go on the wire
const bodyString = JSON.stringify(webhookBody);
const signature = crypto.createHmac('sha256', secret).update(bodyString).digest('hex');
const body = bodyString;
// headers carry the signature; request sends `body` unchanged Defensive patterns
Strategy: validation
Validate before calling
// Run before invoking the webhook command
function validateWebhookCall(webhookConfig, webhookBody) {
const problems = [];
if (!webhookConfig?.url) problems.push('webhookConfig.url is empty — wrong env config');
if (typeof webhookBody !== 'object' || webhookBody === null) problems.push('webhookBody must be an object');
if (webhookConfig?.webhookSecret && typeof webhookConfig.webhookSecret !== 'string') {
problems.push('webhookSecret present but not a string');
}
if (problems.length) throw new Error(`Webhook pre-flight failed: ${problems.join('; ')}`);
}
validateWebhookCall(webhookConfig, webhookBody); Type guard
// Narrows the response body so you branch on a known envelope instead of guessing
function isApiErrorBody(body) {
return (
typeof body === 'object' &&
body !== null &&
'error' in body &&
typeof body.error === 'object' &&
body.error !== null &&
typeof body.error.code === 'string' &&
typeof body.error.message === 'string'
);
} Try / catch
// Cypress commands cannot be try/caught by the caller, so make the failure informative:
// branch on status with expect() instead of a blind throw inside the command's .then().then((response) => {
if (response.status !== 200 && isApiErrorBody(response.body)) {
Cypress.log({ name: 'webhook', message: `code=${response.body.error.code} msg=${response.body.error.message}` });
}
expect(response.status, 'webhook status').to.eq(200);
return cy.wrap(response);
});
// If wrapping in a plain async helper instead of a cy command, the caller may use:
// try { await postWebhook(...) } catch (e) { if (/Webhook failed with error code/.test(e.message)) { /* inspect code, decide retry */ } else throw e; } Prevention
- Keep the webhook secret in the test env in lockstep with the dashboard — rotate both together.
- Always sign the exact byte string you send (assign body = bodyString once and reuse it for HMAC).
- Pin completeUrl per environment in config rather than composing it ad hoc in tests.
- Keep failOnStatusCode: false and branch on status so the x-request-id and error envelope reach your logs before any throw.
- When the endpoint validates timestamps, use a clock-aware header and never cache signed payloads across runs.
When it happens
Trigger: Any non-200 from the webhook endpoint: 401/403 when the signature (webhookSecret HMAC) is missing, wrong, or computed over a different byte sequence than the body actually sent; 404 when completeUrl points at a disabled or wrong-path route; 400 when the payload fails the endpoint's validation; 5xx when the downstream server errors. Also triggered when webhookSecret exists so the command signs the body, but the receiving side expects a different signing scheme or header name.
Common situations: Webhook secret rotated in the dashboard but not in the test env (or vice versa); signing JSON.stringify of a re-serialized object instead of the exact string sent; base URL pointing at the wrong environment/tenant where the webhook is not registered; webhook receiver disabled or the route changed in a newer server version; clock/timestamp skew if the endpoint validates a timestamp header.
Related errors
- computeHmac: 'key' and 'message' are required (got key=${!!k
- Customer create call failed with status: ${response.status}
- Unsupported connector: ${connectorId}
- Unsupported payment method type: ${payment_method_type}
- Failed to fetch QR code image: ${response.statusText}
AI-assisted analysis of juspay/hyperswitch@9b8b89dc37 (2026-08-16).
Data as JSON: /api/errors/dc86c59fc2b12bcf.
Report an issue: GitHub.