skip to content

In a Cypress `req.continue()` handler, why is `res.body.alerts` undefined?

level: seniorimportance: should knowfreq 42%

answer

  1. Nothing threw, but nothing changed
  2. Ask what type the body is
  3. One response header decides it
  4. Valid JSON is not always parsed JSON
  5. Parse, then reassign the whole body

basics

~20 s

Because res.body is a string, not an object. Cypress parses a response body as JSON only when the response carries a Content-Type of application/json; without that header it yields the raw text, so property access returns undefined. Parse it yourself first.

solid answer

~40 s

Cypress decides how to yield `res.body` from the response's own `Content-Type`. With `application/json` and a valid JSON payload you get an object; without that header you get the raw string, even when the text is valid JSON, and binary content arrives as a buffer. Reading `res.body.alerts` off a string gives `undefined`, and nothing throws — the response passes through untouched and the failure surfaces later as a UI assertion. Responses that commonly omit the header include a `304 Not Modified`, a redirect body, and endpoints that never set a content type. The fix is to branch on the type: parse with `JSON.parse()` when `typeof res.body === 'string'`, then reassign the whole body or call `res.send({ body })`, rather than mutating a value that was never parsed.

code

javascript · 13 lines
javascript
cy.intercept('GET', '/api/alerts', (req) => {
  req.continue((res) => {
    // res.body is a string unless the response says content-type: application/json
    const parsed =
      typeof res.body === 'string' ? JSON.parse(res.body) : res.body

    res.send({
      statusCode: res.statusCode,
      body: { ...parsed, alerts: [] },
      headers: { 'content-type': 'application/json' },
    })
  })
})

go deeper

for a junior

Remember that a response body handed to you is not automatically an object. Check what type you are holding before reaching for a property on it.

for a middle

Explain which header drives the parsing, and show both fixes: parse and reassign the body, or set the content type before the browser sees the response.

for a senior

Work backwards from a silent failure - the route matched, the handler ran, the page still shows live data - to the type of res.body, using the recorded response headers rather than guesswork.

for a principal

Decide whether the team's suites are allowed to depend on real responses at all, given that a header change on the backend can turn a passing shaping handler into a silent no-op.

## Where a response handler gets its body There are two ways into the response phase of a `cy.intercept()`, and both hand you the same `res` object: ```js cy.intercept('GET', '/api/alerts', (req) => { req.continue((res) => { // the real response, on its way back to the browser }) req.on('response', (res) => { // same object, a different point in the lifecycle }) }) ``` `res` carries `body`, `headers`, `statusCode` and `statusMessage`, and every one of them is writable: whatever you change is merged into the response the browser finally receives. So `res.body.alerts = []` is the natural way to blank the alert list on an otherwise real response — as long as `res.body` is an object. ## Why the body is sometimes a string Cypress decides whether to parse the body by looking at one header. **If the response carries `Content-Type: application/json` and the body is valid JSON, `res.body` is an object. If that header is missing, `res.body` is the raw string**, even when the text is perfectly good JSON. That is the whole explanation for `res.body.alerts` being `undefined`: you are reading a property off a string, and a string has no `alerts`. Responses that commonly arrive without the header include: - endpoints that write the body themselves and never set a content type; - a `304 Not Modified`, which is not expected to carry representation metadata at all; - a `301` or `302` redirect body; - anything served by a middleware or CDN that rewrote or dropped the header. Binary responses behave differently again: those arrive as a buffer. ## What to do instead 1. **Check the type before you touch it.** Branch on `typeof res.body === 'string'` and parse. 2. **Reassign the whole body**, rather than mutating a value you did not parse. 3. **Set the header if you are shaping the response anyway** — adding `res.headers['content-type'] = 'application/json'` before the browser sees it makes the app parse it too, which is sometimes the actual bug you have just found. ```js cy.intercept('GET', '/api/alerts', (req) => { req.continue((res) => { const parsed = typeof res.body === 'string' ? JSON.parse(res.body) : res.body res.send({ body: { ...parsed, alerts: [] } }) }) }) ``` `res.send()` ends the response phase immediately and merges what you pass with the real response. Its shorthand mirrors `req.reply()`: `res.send(body)`, `res.send(body, headers)` and `res.send(statusCode, body, headers)`. Calling it means no further response handler for that request runs. ## The order the response callbacks run in When several hooks are attached, the sequence is fixed, and getting it wrong produces the same "my edit did nothing" symptom: | Step | What runs | |---|---| | 1 | every `req.on('before:response')` listener | | 2 | the callback passed to `req.continue()` — at most one per request | | 3 | every `req.on('response')` listener | | 4 | the response is delivered to the browser | | 5 | every `req.on('after:response')` listener, whose edits change nothing | So an edit made in an `after:response` listener is always too late, and a `res.send()` early in the chain silently skips the handler you expected to run later. Returning a promise from any of these callbacks makes Cypress await it before moving on. ## The rest of the response you can shape `res.body` is the property that trips people up, but it is not the only one a handler can change, and knowing the full set often removes the need to parse anything: - `res.statusCode` — turn a real 200 into the 503 the error banner is supposed to render. - `res.headers` — add, replace or delete a response header before the browser sees it. - `res.body` — the payload, subject to the parsing rule above. Edits to any of these are merged into the real response when the phase ends, so a handler can leave the body entirely alone and still change how the application behaves. If all you need is a different status code on an otherwise genuine response, that is a one-line handler with no JSON handling in it at all. The timing-related properties on `res` exist too, and belong to the fault-and-delay side of reply shaping rather than here. ## Getting it wrong quietly The failure mode is what makes this a debugging question rather than a documentation lookup. Nothing throws: the route matched, the handler ran, the response went through untouched, and the dashboard renders the live alerts your test was supposed to have suppressed. The assertion that fails is three steps downstream and blames the UI. Two checks settle it quickly: - Log the body's type inside the handler, not its value. `typeof res.body` answers the question immediately; a printed body looks identical either way. - Click the request in the Command Log and read the response headers Cypress recorded. If there is no JSON content type, you have your answer, and the fix belongs in the handler rather than in the assertion.

  • How do `req.continue((res) => {})` and `req.on('response', (res) => {})` differ in a Cypress handler?
    Both receive the real response and both can edit it, but they sit at different points and have different limits. There can be only one `req.continue()` callback per request, and it also completes the request phase; `req.on('response')` can be registered many times and runs after the `req.continue()` callback. `req.on('after:response')` runs once the browser already has the response, so edits there change nothing.
  • What does `res.send()` do that assigning to `res.body` does not?
    Assigning to `res.body` records an edit that is merged when the response phase finishes running its handlers. `res.send()` ends the phase immediately: it merges the `StaticResponse` you pass and skips every remaining response handler for that request. Use assignment when later handlers should still get a turn, and `res.send()` when this handler is deciding the final answer.

saying these in an interview costs you the question

  • Assumes any JSON-looking body is already parsed
  • Blames the application when the edit vanishes
  • Mutates res.body without checking its type
  • Edits the response in an after:response listener
  • Thinks a missing content type is always a Cypress bug