loaders.gl icon indicating copy to clipboard operation
loaders.gl copied to clipboard

[Bug] TypeError: Failed to execute 'text' on 'Response': body stream already read

Open davidcalhoun opened this issue 1 year ago • 4 comments

We have a setup where unfortunately empty tiles are returned from the server with a HTTP 500 Internal Server Error status code and non-JSON text message, e.g. Tile 10/229/410 not found in foo - we're still trying to fix this to instead serve HTTP 204 No Content, but this is a separate issue. For context, we're using TileLayer in @[email protected]

Unfortunately trying to load the empty tiles (with HTTP 500 code and non-JSON text) seems to trigger a bad codepath in @[email protected]:

  • Chrome 125.0.6422.114: Uncaught (in promise) TypeError: Failed to execute 'text' on 'Response': body stream already read
  • Firefox 126.0.1: Uncaught (in promise) TypeError: Response.text: Body has already been consumed.

The line that triggers it the response.text() here (in core/src/lib/utils/response-utils.ts):

  try {
    const contentType = response.headers.get('Content-Type');
    info.reason = contentType?.includes('application/json')
      ? await response.json()
      : response.text();
  } catch (error) {
    // eslint forbids return in a finally statement, so we just catch here
  }

davidcalhoun avatar Jun 03 '24 18:06 davidcalhoun

@davidcalhoun Thanks for reporting, putting up a fix.

It would be good to check response status already in the TileLayer and avoid parsing the Response body there so that we can handle missing tiles correctly and take full advantage of error handling logic in loaders.gl.

So it would be interesting to see the stack trace that leads here if you have - just in case it pinpoints things.

ibgreen avatar Jun 03 '24 19:06 ibgreen

@ibgreen Great, thank you for the quick response!

Here's the stack trace - I added some annotations so you can see the line of code it's traced to:

response-utils.js:83 Uncaught (in promise) TypeError: Failed to execute 'text' on 'Response': body stream already read
    at y (response-utils.js:83:1)  // `: response.text();`
    at m (response-utils.js:54:1) // `const error = await getResponseError(response);`
    at Y (get-data.js:58:15) // `await checkResponse(response);`
    at eu (parse.js:66:18) // `data = await getArrayBufferOrStringFromData(data, loader, options);`
    at el (parse.js:51:1) // `return await parseWithLoader(loader, data, options, context);`
    at async ec (load.js:39:1) // `? await parse(data, resolvedLoaders, resolvedOptions) // loader array overload`
    at async nF._loadData (tile-2d-header.js:81:1) // (deck.gl) `tileData = await getData({ index, id, bbox, userData, zoom, signal });`

davidcalhoun avatar Jun 03 '24 19:06 davidcalhoun

That is what I was looking for. At first blush, it looks like parse() here in loaders.gl doesn't check the response status before trying to read the data.

ibgreen avatar Jun 03 '24 19:06 ibgreen

Fix landed but unfortunately ran into some publishing issues on 4.2 branch... Apologies, will look again when I have more time

ibgreen avatar Jun 03 '24 19:06 ibgreen

Closing this out. Thank you @ibgreen for fixing this in https://github.com/visgl/loaders.gl/pull/3026 !

davidcalhoun avatar Nov 27 '24 19:11 davidcalhoun