Describe the bug
A byte-range request made by HTTPFileSource (Resource::dataRange) that is answered with 200 OK (the full resource) instead of 206 Partial Content is buffered in full, with no size cap.
So when the resource is large, for example a multi-GB PMTiles archive, mbgl holds the whole file in RAM and the process dies with std::bad_alloc / OOM. The map also goes blank because tiles are never decoded.
In platform/default/src/mbgl/storage/http_file_source.cpp, HTTPRequest::writeCallback appends every received chunk unconditionally, and HTTPRequest::handleResult only inspects the status code after the whole body has already been downloaded.
This happened in production on real tile infrastructure that is fronted by the Cloudflare CDN. Cloudflare intermittently answered Range requests for a ~83 GB PMTiles archive with 200 (the full file) instead of 206, so the client buffered the entire file and OOM-crashed while the map stayed blank.
To Reproduce
End to end (reproduces the OOM): point any maplibre-native map at this style, whose pmtiles:// source is served with 200 for every range request (it is the real ~83 GB file):
https://broken-range-request.yuiseki.com/styles/osm-bright.json
Quick check with curl, a 100-byte range comes back as 200 with the full 83 GB content-length, not 206:
$ curl -sD- -o /dev/null -H "Range: bytes=0-99" https://broken-range-request.yuiseki.com/planet.pmtiles
HTTP/2 200
content-length: 82984871716
accept-ranges: bytes
Requesting a small byte range through HTTPFileSource against that URL (a Resource with dataRange = {0, 126}) then buffers the whole body into Response.data with no error, and with the real file the allocation is unbounded and the process OOMs.
Expected behavior
A range request that receives a 200 (the server ignored Range) should be treated as an error, and the transfer should be aborted before the body is buffered, instead of downloading the entire resource into memory.
Screenshots
Not applicable.
Platform information (please complete the following information):
- Operating System: Ubuntu 24.04 LTS (x86_64)
- Platform: C++ core, Linux default platform (
platform/default, libcurl HTTP source) and maplibre-native-slint
- Version: reproduced on maplibre-native
04c4507 (vendored via maplibre-native-slint); the affected code is unchanged on main.
Additional context
Related: #3658 is another PMTiles client crash with a different root cause (a stale header after the file is replaced), and maplibre/demotiles#35 is Cloudflare corrupting PMTiles Range responses, which was fixed on the server side. Both show that CDNs mishandling Range for PMTiles happens in the wild; this issue is about the client not OOMing when that happens.
I have a verified patch that treats a range request answered with 200 as an error and aborts the transfer before the body is buffered.
Against the endpoint above it stops immediately (dataSize=0) instead of buffering the full body.
If this is just an implementation oversight, I can send a PR right away.
AI Disclosure
I used Claude Code Opus 4.8 to investigate, reproduce and try to fix this issue.
Describe the bug
A byte-range request made by
HTTPFileSource(Resource::dataRange) that is answered with200 OK(the full resource) instead of206 Partial Contentis buffered in full, with no size cap.So when the resource is large, for example a multi-GB PMTiles archive, mbgl holds the whole file in RAM and the process dies with
std::bad_alloc/ OOM. The map also goes blank because tiles are never decoded.In
platform/default/src/mbgl/storage/http_file_source.cpp,HTTPRequest::writeCallbackappends every received chunk unconditionally, andHTTPRequest::handleResultonly inspects the status code after the whole body has already been downloaded.This happened in production on real tile infrastructure that is fronted by the Cloudflare CDN. Cloudflare intermittently answered
Rangerequests for a ~83 GB PMTiles archive with200(the full file) instead of206, so the client buffered the entire file and OOM-crashed while the map stayed blank.To Reproduce
End to end (reproduces the OOM): point any maplibre-native map at this style, whose
pmtiles://source is served with200for every range request (it is the real ~83 GB file):Quick check with curl, a 100-byte range comes back as
200with the full 83 GBcontent-length, not206:Requesting a small byte range through
HTTPFileSourceagainst that URL (aResourcewithdataRange = {0, 126}) then buffers the whole body intoResponse.datawith no error, and with the real file the allocation is unbounded and the process OOMs.Expected behavior
A range request that receives a
200(the server ignoredRange) should be treated as an error, and the transfer should be aborted before the body is buffered, instead of downloading the entire resource into memory.Screenshots
Not applicable.
Platform information (please complete the following information):
platform/default, libcurl HTTP source) andmaplibre-native-slint04c4507(vendored via maplibre-native-slint); the affected code is unchanged onmain.Additional context
Related: #3658 is another PMTiles client crash with a different root cause (a stale header after the file is replaced), and maplibre/demotiles#35 is Cloudflare corrupting PMTiles
Rangeresponses, which was fixed on the server side. Both show that CDNs mishandlingRangefor PMTiles happens in the wild; this issue is about the client not OOMing when that happens.I have a verified patch that treats a range request answered with
200as an error and aborts the transfer before the body is buffered.Against the endpoint above it stops immediately (
dataSize=0) instead of buffering the full body.If this is just an implementation oversight, I can send a PR right away.
AI Disclosure
I used Claude Code Opus 4.8 to investigate, reproduce and try to fix this issue.