Skip to content

Fix ESRIGeoJSON: handle Empty reply from server and paginate past maxRecordCount - #52

Merged
njakobsen merged 3 commits into
masterfrom
fix-esri-geojson-empty-reply
Aug 17, 2026
Merged

Fix ESRIGeoJSON: handle Empty reply from server and paginate past maxRecordCount#52
njakobsen merged 3 commits into
masterfrom
fix-esri-geojson-empty-reply

Conversation

@rywall

@rywall rywall commented May 8, 2026

Copy link
Copy Markdown
Member

Summary

  • Empty reply from server: ESRIGeoJSON passed remote URLs straight to ogr2ogr, which uses GDAL's curl client. Some ArcGIS-on-IIS endpoints return an empty reply to that client (likely an HTTPS 1.1 negotiation issue — GDAL's driver list hints at this with MapInfo File' needs https 1.1), producing ERROR 1: Empty reply from server. Fix: fetch the URL with Ruby's `open-uri` into a tempfile, then run `ogr2ogr` on the local path.
  • Truncation at maxRecordCount: ArcGIS query endpoints cap each response at the service's `maxRecordCount` (commonly 1000 or 2000 features) and signal `exceededTransferLimit` when more results exist. Fix: walk the pages with `resultOffset` and merge them into a single FeatureCollection before handing to `ogr2ogr`.
  • Local-path behavior is unchanged.

Test plan

  • Import features from an ArcGIS-on-IIS REST `query?...&f=geojson` URL that previously failed with `Empty reply from server`.
  • Import features from an ArcGIS query URL whose result set exceeds `maxRecordCount` and confirm all features are returned (not just the first page).
  • Import features from an ArcGIS query URL whose result set fits in one page and confirm a single request is still made.
  • Import features from a local `.geojson` / `.json` file (regression check for the relative-path branch).

🤖 Generated with Claude Code

@rywall rywall changed the title Fix ESRIGeoJSON import failing with "Empty reply from server" Fix ESRIGeoJSON: handle Empty reply from server and paginate past maxRecordCount May 8, 2026
@njakobsen
njakobsen force-pushed the fix-esri-geojson-empty-reply branch from 9d511cb to 2e53ff1 Compare August 17, 2026 05:18
rywall and others added 2 commits August 16, 2026 22:50
GDAL's curl-based fetcher fails with "Empty reply from server" against
some ArcGIS endpoints (e.g. Cloudflare-fronted servers that only offer
HTTPS 1.1). Fetch remote URLs with Ruby's open-uri and hand ogr2ogr a
local tempfile instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ArcGIS query endpoints truncate each response at the service's
maxRecordCount (commonly 1000 or 2000 features) and set
exceededTransferLimit when more results exist. Walk the pages with
resultOffset and merge them into a single FeatureCollection before
handing to ogr2ogr.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@njakobsen
njakobsen force-pushed the fix-esri-geojson-empty-reply branch 3 times, most recently from a478c9f to 454e3e7 Compare August 17, 2026 06:19
Comment thread lib/spatial_features/importers/esri_geo_json.rb Outdated
Comment thread lib/spatial_features/importers/esri_geo_json.rb
`ESRIGeoJSON` walks an ArcGIS query endpoint's pages until one comes back without `exceededTransferLimit`. A server that does not honour `resultOffset` answers every request with the same first page and that flag still set, so the walk never reaches a last page and the collection grows until the process runs out of memory.

The walk now stops after `max_pages` requests and raises, rather than writing a collection that is missing whatever came after the cap. A page that does not parse as JSON, and an endpoint that cannot be reached at all, both raise `ImportError` naming what happened instead of surfacing `JSON::ParserError` or `OpenURI::HTTPError`. Each request is bounded by `request_timeout`, since a hung endpoint would otherwise hold an import worker open indefinitely. `max_pages` and `request_timeout` are `class_attribute`s, defaulting to 500 and 60 seconds, which a deployment with a larger layer or a slower service can raise.

Adds the importer's first specs, covering a single page, a walk across several pages, the `resultOffset` each request asks for, the flag nested under `properties`, a runaway server, an unparseable reply, an unreachable one, the request timeout, a local path read without downloading, and a local path holding shell metacharacters. Both response shapes are exercised: the GeoJSON an `f=geojson` query returns and the ESRI JSON an `f=json` query returns, the latter being the format the class is named for and the one OGR has to sniff from the content. They stub `URI` the way `kml_file_arcgis_spec.rb` stubs `Download`, so no new test dependency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@njakobsen

Copy link
Copy Markdown
Member

Both points addressed, and acting on them turned up two defects that were not visible before.

SpatialFeatures::GDAL

New module wrapping the GDAL command line tools. GDAL.capture(tool, *args) returns stdout, GDAL.run(tool, *args) returns the exit status. All three call sites now route through it — ogr2ogr in ESRIGeoJSON, and ogr2ogr plus gdalsrsinfo in Shapefile — where previously each rolled its own invocation in a different style (Open3.capture2, bare system, and a third form).

It also closes a hazard none of the individual call sites had guarded. system("one string") and Open3 both fall back to a shell when handed a lone string, so a future caller passing one argument would silently reintroduce shell parsing. The module always spawns via [[tool, tool], *args], which cannot fall back.

Download

Rather than a new module, the existing SpatialFeatures::Download gained what ESRIGeoJSON had grown privately: Download.read for a body as a String, a configurable Download.timeout applied to every remote fetch, and one UNREACHABLE_ERRORS rescue mapping network failures to ImportError. ESRIGeoJSON no longer carries its own copy of any of that.

Two defects this surfaced

Download.entries ran arbitrary commands. It used Kernel#open, which executes its argument when the string begins with a pipe — Download.entries("|touch /tmp/x") created the file. No caller inside the gem reaches it, but it is public API.

My first fix was wrong, and the spec I wrote to cover it caught that: URI.open hands anything that is not a URL straight to Kernel#open, so swapping one for the other changed nothing. A local path now goes to File.open, which cannot execute.

Centralising the rescue reintroduced a fixed disclosure. Errno::ENOENT is a SystemCallError, so a UNREACHABLE_ERRORS rescue covering local paths turned a missing upload into "This source could not be reached. No such file or directory @ rb_sysopen - /home/.../uploads/..." — putting the server path back in front of the submitter, which 225b8fa had removed. Caught by that commit's own spec. The rescue now applies only to remote URLs.

Verification

302 examples, 0 failures. New download_spec.rb covers the pipe execution on all three entry points, the timeout, the unreachable mapping, and that a missing local path still raises Errno::ENOENT rather than being reported as unreachable.

Mutation-tested: reverting the argv list to a shell string fails 2, routing a local path back through URI.open fails 1, and dropping the timeouts fails 2.

🤖 Generated with Claude Code

https://claude.ai/code/session_017uHh49RG66T3qNFNRnPY6s

@njakobsen
njakobsen force-pushed the fix-esri-geojson-empty-reply branch from 454e3e7 to 232983b Compare August 17, 2026 06:51
@njakobsen
njakobsen merged commit bda8d82 into master Aug 17, 2026
2 checks passed
@njakobsen
njakobsen deleted the fix-esri-geojson-empty-reply branch August 17, 2026 06:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants