Send custom HTTP request headers when downloading a source - #103
Merged
iandees merged 7 commits intoAug 18, 2026
Conversation
guess_url_file_extension() made its own GET request to sniff the Content-Type before the real download, but never carried any custom headers. On a header-gated host (e.g. one requiring Referer) that pre-flight request would fail even after headers are wired through the rest of the download path.
…nloads DownloadTask.__init__ already accepted a headers dict, but from_protocol_string() - the only production call site - never passed one through, so self.headers was always just the default User-Agent. EsriRestDownloadTask also built its EsriDumper without headers despite pyesridump already supporting extra_headers.
Wires the source-supplied 'headers' dict (added to the schema separately in openaddresses/openaddresses) into the download path. conform() deliberately does not forward headers: it re-downloads from the OA-owned cache artifact, not the contributor's host.
Covers: from_protocol_string() forwarding headers to URLDownloadTask and EsriRestDownloadTask, default User-Agent survives alongside custom headers (and can be overridden), headers reaching both the extension-guessing pre-flight request and the real download request, and EsriRestDownloadTask passing headers to EsriDumper as extra_headers.
Fill in the CHANGELOG's PR link once this is merged.
Review feedback on openaddresses/openaddresses#8306: wrapping headers in a container leaves room for query params (e.g. a token) later without a second schema migration. No source uses the flat key yet, so this costs nothing now.
Further review feedback on openaddresses/openaddresses#8306: iandees agreed 'request' is shorter and reads better than http_request_settings. Source key is now request.headers.
Contributor
Author
|
Blocked by #104 |
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #102. Implements openaddresses/openaddresses#1745 and openaddresses/openaddresses#8306. Unblocks openaddresses/openaddresses#4550 (Champaign, IL) and likely openaddresses/openaddresses#6226 (Schuylkill County, PA).
What
Reads a source's
request.headers(added to the schema in openaddresses/openaddresses#8306) and sends those headers on every download request for that source — e.g. so a source that 403s without aRefererheader can finally be fetched.{ "name": "champaign", "protocol": "http", "data": "https://example.gov/addresses.geojson", "request": { "headers": { "Referer": "https://example.gov/gis/" } } }Why this was more than a schema-wiring task
DownloadTask.__init__already accepted aheadersdict and merged it over the defaultUser-Agent, andURLDownloadTask.downloadalready sentself.headerson its request — that plumbing predates this PR. But nothing actually reached it:DownloadTask.from_protocol_string, the only production constructor path, took noheadersargument.cache()never read aheaders/requestkey off the source config.EsriRestDownloadTaskbuilt itsEsriDumperwithout headers, even though pyesridump already supportsextra_headers.guess_url_file_extensionmakes its own separate, un-headeredGETto sniff the file's Content-Type before the real download runs. Even with everything else fixed, a source gated onRefererwould still fail at this pre-flight step — this was the part most easily missed, since it isn't in the obvious download path.All four are fixed here.
Changes
openaddr/cache.pyguess_url_file_extension()now accepts and sendsheaderson its pre-flight sniffing request;URLDownloadTask.get_file_path()passesself.headersthrough.DownloadTask.from_protocol_string()acceptsheadersand forwards it to bothURLDownloadTaskandEsriRestDownloadTask.EsriRestDownloadTask.download()passesextra_headers=self.headerstoEsriDumper.openaddr/__init__.pycache()readssource_config.data_source['request']['headers']and passes it down.conform()deliberately does not forward headers — it re-downloads from the OA-owned cache artifact (S3), not the contributor's original host, so contributor-supplied headers don't apply there. Left a comment explaining this so it doesn't read as an oversight.openaddr/tests/cache.py— new coverage:task.headersviafrom_protocol_string()for bothhttpandESRIUser-Agentsurvives alongside custom headers, and can be overriddenEsriRestDownloadTaskpasses headers toEsriDumperasextra_headerscache()correctly readsrequest.headersfrom the source config end-to-end, and a source with norequestblock at all still worksopenaddr/VERSION/CHANGELOG— bumped to10.1.0Naming
Headers are nested under a
requestobject (request.headers) rather than a flatheaderskey, per discussion in openaddresses/openaddresses#8306 — this leaves room for a future sibling setting (e.g. a query-param token) without a second schema migration. The key went through a couple of names during review (headers→http_request_settings→request); the commit history reflects that back-and-forth in case it's useful context, but the net result isrequest.headers, matching the schema.The schema side (openaddresses/openaddresses#8306,
headers-field-supportbranch) is already updated to match this shape. This PR should merge and be released before that schema PR merges — otherwise a source could pass schema validation while this repo is still on the old version and silently ignores the field.Testing
Local Python lacks
gdal/shapely; verified against the project'sbatch-machine:latestDocker image:docker run --rm -v "$(pwd)":/app -w /app batch-machine:latest python -m unittest openaddr.tests.cache openaddr.tests -v57 tests, all passing. Also manually confirmed the new
cache()test fails correctly when the config key is misspelled, to make sure it actually exercises the wiring rather than passing vacuously.