From 04d08f1258e1183c55cf8f5d1fb39015fff73aab Mon Sep 17 00:00:00 2001 From: Brendan Ward Date: Wed, 17 Jan 2024 09:51:41 -0800 Subject: [PATCH] ENH: Add support for returning missing images as HTTP 404 instead of blank PNGs (#177) --- CHANGELOG.md | 4 +++ README.md | 58 +++++++++++++++++++++++++++--------------- handlers/serviceset.go | 49 ++++++++++++++++++----------------- handlers/tileset.go | 19 +++++++++----- main.go | 20 +++++++++------ 5 files changed, 91 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c564c36..1e9e334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.11.0 (in development) +- support returning missing image tiles as HTTP 404 instead of blank tiles using + the `--missing-image-tile-404` option (#177). + + ## 0.10.0 - supports GCC11 on Ubuntu 22.04 (#166) diff --git a/README.md b/README.md index 8dd1bbf..c88f252 100644 --- a/README.md +++ b/README.md @@ -53,32 +53,36 @@ From within the repository root ($GOPATH/bin needs to be in your $PATH): ``` $ mbtileserver --help -Serve tiles from mbtiles files. +Serve tiles from mbtiles files Usage: mbtileserver [flags] Flags: - -c, --cert string X.509 TLS certificate filename. If present, will be used to enable SSL on the server. - -d, --dir string Directory containing mbtiles files. Directory containing mbtiles files. Can be a comma-delimited list of directories. (default "./tilesets") - --disable-preview Disable map preview for each tileset (enabled by default) - --disable-svc-list Disable services list endpoint (enabled by default) - --disable-tilejson Disable TileJSON endpoint for each tileset (enabled by default) - --domain string Domain name of this server. NOTE: only used for AutoTLS. - --dsn string Sentry DSN - --enable-arcgis Enable ArcGIS Mapserver endpoints - --enable-fs-watch Enable reloading of tilesets by watching filesystem - --enable-reload-signal Enable graceful reload using HUP signal to the server process - --generate-ids Automatically generate tileset IDs instead of using relative path - -h, --help help for mbtileserver - -k, --key string TLS private key - -p, --port int Server port. Default is 443 if --cert or --tls options are used, otherwise 8000. (default -1) - -r, --redirect Redirect HTTP to HTTPS - --root-url string Root URL of services endpoint (default "/services") - -s, --secret-key string Shared secret key used for HMAC request authentication - --tiles-only Only enable tile endpoints (shortcut for --disable-svc-list --disable-tilejson --disable-preview) - -t, --tls Auto TLS via Let's Encrypt - -v, --verbose Verbose logging + --basemap-style-url string Basemap style URL for preview endpoint (can include authorization token parameter if required by host) + --basemap-tiles-url string Basemap raster tiles URL pattern for preview endpoint (can include authorization token parameter if required by host): https://some.host/{z}/{x}/{y}.png + -c, --cert string X.509 TLS certificate filename. If present, will be used to enable SSL on the server. + -d, --dir string Directory containing mbtiles files. Can be a comma-delimited list of directories. (default "./tilesets") + --disable-preview Disable map preview for each tileset (enabled by default) + --disable-svc-list Disable services list endpoint (enabled by default) + --disable-tilejson Disable TileJSON endpoint for each tileset (enabled by default) + --domain string Domain name of this server. NOTE: only used for AutoTLS. + --dsn string Sentry DSN + --enable-arcgis Enable ArcGIS Mapserver endpoints + --enable-fs-watch Enable reloading of tilesets by watching filesystem + --enable-reload-signal Enable graceful reload using HUP signal to the server process + --generate-ids Automatically generate tileset IDs instead of using relative path + -h, --help help for mbtileserver + --host string IP address to listen on. Default is all interfaces. (default "0.0.0.0") + -k, --key string TLS private key + --missing-image-tile-404 Return HTTP 404 error code when image tile is misssing instead of default behavior to return blank PNG + -p, --port int Server port. Default is 443 if --cert or --tls options are used, otherwise 8000. (default -1) + -r, --redirect Redirect HTTP to HTTPS + --root-url string Root URL of services endpoint (default "/services") + -s, --secret-key string Shared secret key used for HMAC request authentication + --tiles-only Only enable tile endpoints (shortcut for --disable-svc-list --disable-tilejson --disable-preview) + -t, --tls Auto TLS via Let's Encrypt + -v, --verbose Verbose logging ``` So hosting tiles is as easy as putting your mbtiles files in the `tilesets` @@ -306,6 +310,18 @@ These are provided at: where `` is one of `png`, `jpg`, `webp`, `pbf` depending on the type of data in the tileset. + +### Missing tiles + +Missing vector tiles are always returned as HTTP 204. + +Missing image tiles are returned as blank PNGs with the same dimensions as the tileset to give seamless display of +these tiles in interactive maps. + +When serving image tiles that encode data (e.g., terrain) instead of purely for display, this can cause issues. In +this case, you can use the `--missing-image-tile-404` option. This behavior will be applied to all image tilesets. + + ## TileJSON API `mbtileserver` automatically creates a TileJSON endpoint for each service at `/services/`. diff --git a/handlers/serviceset.go b/handlers/serviceset.go index e272ebd..333f3fd 100644 --- a/handlers/serviceset.go +++ b/handlers/serviceset.go @@ -13,14 +13,15 @@ import ( // ServiceSetConfig provides configuration options for a ServiceSet type ServiceSetConfig struct { - EnableServiceList bool - EnableTileJSON bool - EnablePreview bool - EnableArcGIS bool - BasemapStyleURL string - BasemapTilesURL string - RootURL *url.URL - ErrorWriter io.Writer + EnableServiceList bool + EnableTileJSON bool + EnablePreview bool + EnableArcGIS bool + BasemapStyleURL string + BasemapTilesURL string + ReturnMissingImageTile404 bool + RootURL *url.URL + ErrorWriter io.Writer } // ServiceSet is a group of tilesets plus configuration options. @@ -28,12 +29,13 @@ type ServiceSetConfig struct { type ServiceSet struct { tilesets map[string]*Tileset - enableServiceList bool - enableTileJSON bool - enablePreview bool - enableArcGIS bool - basemapStyleURL string - basemapTilesURL string + enableServiceList bool + enableTileJSON bool + enablePreview bool + enableArcGIS bool + basemapStyleURL string + basemapTilesURL string + returnMissingImageTile404 bool rootURL *url.URL errorWriter io.Writer @@ -48,15 +50,16 @@ func New(cfg *ServiceSetConfig) (*ServiceSet, error) { } s := &ServiceSet{ - tilesets: make(map[string]*Tileset), - enableServiceList: cfg.EnableServiceList, - enableTileJSON: cfg.EnableTileJSON, - enablePreview: cfg.EnablePreview, - enableArcGIS: cfg.EnableArcGIS, - basemapStyleURL: cfg.BasemapStyleURL, - basemapTilesURL: cfg.BasemapTilesURL, - rootURL: cfg.RootURL, - errorWriter: cfg.ErrorWriter, + tilesets: make(map[string]*Tileset), + enableServiceList: cfg.EnableServiceList, + enableTileJSON: cfg.EnableTileJSON, + enablePreview: cfg.EnablePreview, + enableArcGIS: cfg.EnableArcGIS, + basemapStyleURL: cfg.BasemapStyleURL, + basemapTilesURL: cfg.BasemapTilesURL, + returnMissingImageTile404: cfg.ReturnMissingImageTile404, + rootURL: cfg.RootURL, + errorWriter: cfg.ErrorWriter, } return s, nil diff --git a/handlers/tileset.go b/handlers/tileset.go index 954cba4..b16816b 100644 --- a/handlers/tileset.go +++ b/handlers/tileset.go @@ -219,7 +219,7 @@ func (ts *Tileset) tileHandler(w http.ResponseWriter, r *http.Request) { if ts == nil || !ts.published { // In order to not break any requests from when this tileset was published // return the appropriate not found handler for the original tile format. - tileNotFoundHandler(w, r, ts.tileformat, ts.tilesize) + tileNotFoundHandler(w, r, ts.tileformat, ts.tilesize, ts.svc.returnMissingImageTile404) return } @@ -255,7 +255,7 @@ func (ts *Tileset) tileHandler(w http.ResponseWriter, r *http.Request) { return } if data == nil || len(data) <= 1 { - tileNotFoundHandler(w, r, ts.tileformat, ts.tilesize) + tileNotFoundHandler(w, r, ts.tileformat, ts.tilesize, ts.svc.returnMissingImageTile404) return } @@ -327,13 +327,18 @@ func (ts *Tileset) previewHandler(w http.ResponseWriter, r *http.Request) { // tileNotFoundHandler is an http.HandlerFunc that writes the default response // for a non-existing tile of type f to w -func tileNotFoundHandler(w http.ResponseWriter, r *http.Request, f mbtiles.TileFormat, tilesize uint32) { +func tileNotFoundHandler(w http.ResponseWriter, r *http.Request, f mbtiles.TileFormat, tilesize uint32, returnMissingImageTile404 bool) { switch f { case mbtiles.PNG, mbtiles.JPG, mbtiles.WEBP: - // Return blank PNG for all image types - w.Header().Set("Content-Type", "image/png") - w.WriteHeader(http.StatusOK) - w.Write(BlankPNG(tilesize)) + if returnMissingImageTile404 { + // Return 404 + w.WriteHeader(http.StatusNotFound) + } else { + // Return blank PNG for all image types + w.Header().Set("Content-Type", "image/png") + w.WriteHeader(http.StatusOK) + w.Write(BlankPNG(tilesize)) + } case mbtiles.PBF: // Return 204 w.WriteHeader(http.StatusNoContent) diff --git a/main.go b/main.go index 8fcc460..357aeec 100644 --- a/main.go +++ b/main.go @@ -82,6 +82,7 @@ var ( tilesOnly bool basemapStyleURL string basemapTilesURL string + missingImageTile404 bool ) func init() { @@ -112,6 +113,8 @@ func init() { flags.StringVar(&basemapStyleURL, "basemap-style-url", "", "Basemap style URL for preview endpoint (can include authorization token parameter if required by host)") flags.StringVar(&basemapTilesURL, "basemap-tiles-url", "", "Basemap raster tiles URL pattern for preview endpoint (can include authorization token parameter if required by host): https://some.host/{z}/{x}/{y}.png") + flags.BoolVarP(&missingImageTile404, "missing-image-tile-404", "", false, "Return HTTP 404 error code when image tile is misssing instead of default behavior to return blank PNG") + flags.BoolVarP(&verbose, "verbose", "v", false, "Verbose logging") if env := os.Getenv("HOST"); env != "" { @@ -292,14 +295,15 @@ func serve() { } svcSet, err := handlers.New(&handlers.ServiceSetConfig{ - RootURL: rootURL, - ErrorWriter: &errorLogger{log: log.New()}, - EnableServiceList: !disableServiceList, - EnableTileJSON: !disableTileJSON, - EnablePreview: !disablePreview, - EnableArcGIS: enableArcGIS, - BasemapStyleURL: basemapStyleURL, - BasemapTilesURL: basemapTilesURL, + RootURL: rootURL, + ErrorWriter: &errorLogger{log: log.New()}, + EnableServiceList: !disableServiceList, + EnableTileJSON: !disableTileJSON, + EnablePreview: !disablePreview, + EnableArcGIS: enableArcGIS, + BasemapStyleURL: basemapStyleURL, + BasemapTilesURL: basemapTilesURL, + ReturnMissingImageTile404: missingImageTile404, }) if err != nil { log.Fatalln("Could not construct ServiceSet")