Guide

MapLibre: “Unable to parse the tile” and “Unimplemented type: 3”

The vector tiles reach the browser, but MapLibre cannot read them and the map stays empty. Almost always the tiles are gzip-compressed and the server does not say so. How to tell the causes apart, and the fix for nginx, Apache, Caddy, S3, GitHub Pages and PMTiles.

The messages

The wording depends on the version. We reproduced each case in Chromium with MapLibre GL JS 3.3.0 and 6.13.0:

The server sendsMapLibre 3.4.0 and laterMapLibre before 3.4.0, Mapbox GL JS
gzip data without Content-Encoding: gzipUnable to parse the tile at …, please make sure the data is not gzipped and that you have configured the relevant header in the serverUnimplemented type: 3
data compressed twice, header sent oncethe same as gzip aboveUnimplemented type: 3
an HTML page (error page, login page, index.html)Unable to parse the tile at …, got error: Unimplemented type: 4Unimplemented type: 4
a PNG image (a raster tile)Unable to parse the tile at …, got error: Unimplemented type: 3Unimplemented type: 3

The number is the field type that the protocol buffer decoder found in the first bytes, so it hints at what the bytes are. It is no error code of the tile server. The message repeats for every tile, so a map can log dozens of them at once.

Look at one tile

Copy a tile URL from the network tab of the browser and request it the way the browser does:

curl -s -D - -o tile.bin -H 'Accept-Encoding: gzip, br' 'https://tiles.example.com/14/8580/5737.pbf' \
  | grep -i -e '^HTTP' -e content-type -e content-encoding
head -c 4 tile.bin | od -An -tx1
First bytesThe body isThe header must be
1aa vector tile, uncompressedno Content-Encoding
1f 8bgzipContent-Encoding: gzip, once
3c (“<”)HTML or XML: an error or fallback pagefix the URL or the routing
7b (“{”)JSON: usually an error message of the APIread it, it names the problem
89 50 4e 47a PNG imageuse a raster source

If the body starts with 1f 8b and the header says Content-Encoding: gzip, unpack it once (gzip -dc tile.bin | head -c 4 | od -An -tx1). If it starts with 1f 8b again, the tile was compressed twice.

Why tiles are gzip-compressed

Vector tiles compress well, so most tools store them compressed: MBTiles files from tippecanoe and planetiler, PMTiles archives, and folders written by mb-util or tippecanoe -e. A tile server such as tileserver-gl or martin knows this and sends the header. A plain web server, a bucket or a CDN sees only files, sends them without the header, and the browser hands compressed bytes to MapLibre.

Fix: send the header

Each of these was tested with gzip tiles in a folder tiles/, with compression switched on for the rest of the site, and curl confirmed one Content-Encoding: gzip and a body that unpacks to a vector tile:

location /tiles/ {
    gzip off;                          # the files are compressed already
    add_header Content-Encoding gzip;
    add_header Access-Control-Allow-Origin *;
    types { application/vnd.mapbox-vector-tile pbf mvt; }
}
# add_header in a location replaces the add_header lines of the server block

Then reload the page with the cache emptied (or open a private window): the browser keeps the broken tiles for as long as the old cache headers allow.

Fix: unpack the tiles

Where you cannot set headers per path, as on GitHub Pages, store the tiles uncompressed. They are then larger on the way to the browser, unless the host compresses them on the fly.

# unpack a folder of gzip tiles in place
find tiles -name '*.pbf' -exec sh -c 'gzip -dc "$1" > "$1.tmp" && mv "$1.tmp" "$1"' _ {} \;

# or write them uncompressed in the first place
tippecanoe -e tiles --no-tile-compression -zg data.geojson

Compressed twice

When the files are gzip already and the server or a CDN compresses them again, the browser undoes one layer and MapLibre gets the other. Turn off on-the-fly compression for the tile path (gzip off in nginx, SetEnv no-gzip in Apache), and check the settings of the CDN, which may compress the type application/x-protobuf.

An HTML page instead of a tile

“got error: Unimplemented type: 4” and first bytes 3c mean that the server sent a web page. Common reasons:

  • A single-page app falls back to index.html for every unknown path (try_files $uri /index.html, rewrites on Netlify or Vercel), so missing tiles come back as the app with status 200. Exclude the tile path from the fallback, and answer missing tiles with 404 or 204.
  • A login page, a proxy error page or a “not found” page with status 200.
  • A wrong URL template: a missing path part, or {z}/{y}/{x} in the wrong order.

PMTiles

Do not unpack a PMTiles archive into a web server folder to fix this. Serve the archive as one file with HTTP range requests and let the pmtiles library read it; it unpacks the tiles itself:

import { Protocol } from "pmtiles";
maplibregl.addProtocol("pmtiles", new Protocol().tile);

// in the style:
// "sources": { "world": { "type": "vector", "url": "pmtiles://https://example.com/world.pmtiles" } }

A raster tile in a vector source

If the tile is a PNG or JPEG, the source in the style must be "type": "raster", with "tileSize": 256 for most raster services. A raster tile set can only be shown as pictures; it has no layers to style.

Or let someone else serve the tiles

overpass.ch serves vector tiles of the whole world with the right headers, ETags and CORS, in nine ready styles for MapLibre, with labels in 41 languages and 3D terrain. 100'000 tiles a month are free. Errors come back as JSON with a code.