ISSUE-21583: httpfs: fall back to full GET when HEAD disallows or Range probe fails#309
Open
herbenderbler wants to merge 1 commit into
Open
ISSUE-21583: httpfs: fall back to full GET when HEAD disallows or Range probe fails#309herbenderbler wants to merge 1 commit into
herbenderbler wants to merge 1 commit into
Conversation
- Skip Range probe when HEAD returns 405/501 (common for REST APIs) - On Range failure or HTTPException, defer to full download when auto_fallback_to_full_download is true - Name Range probe size as HTTP_RANGE_FILE_SIZE_PROBE_LENGTH Fixes duckdb/duckdb#21583
carlopi
pushed a commit
that referenced
this pull request
Jun 25, 2026
Currently when `Range` requests are performed during the file download and the server does not support ranges - then the download logic falls back to use full file download with `GET`. To perform the `Range` requests the client first sends a `HEAD` request to get the `Content-Length`. In case when `HEAD` is not supported by the server, it resorts to using `Range: bytes=0-1` requests instead of `HEAD`. The problem is that these `Range: bytes=0-1` requests (unlike normal download `Range` requests) are throwing on error, so when neither `Range` nor `HEAD` are supported - then the fallback to full `GET` does not happen. This PR allows to fall back from `Range: bytes=0-1` responses to a full `GET` download. Testing: adding an SQLLogic test is untrivial because `PYTHON_HTTP_SERVER_DIR` server currently used on CI only uses default `http.server` handler. A custom handler is required there to be able to disallow `HEAD` on selected requests. Please let me know if this needs to be implemented. So far the change was tested manually on OpenAI API with the following: ```sql statement ok CREATE TEMPORARY SECRET openai ( TYPE http, SCOPE 'https://api.openai.com', EXTRA_HTTP_HEADERS MAP {'Authorization': 'Bearer sk-admin-xxx'} ) query IIIIII FROM read_json('https://api.openai.com/v1/organization/users/user-xxx') ---- user-xxx organization.user 1234567890 alexkasko@ducklabs.com Alex Kasko owner ``` Supersedes: #309 Fixes: duckdb/duckdb#21583
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.
Fixes duckdb/duckdb#21583
Problem
Remote HTTPS URLs used with
read_json/read_json_auto(and similar) go through httpfs’s file metadata path:HEAD, then—whenHEADis not200 OK—aGETwithRange: bytes=0-1to infer size. That matches object storage and static files, but many REST/JSON APIs do not implementHEAD, return405/501, or do not honorRangeon JSON responses.For
https://api.openai.com/...with anhttpsecret, users sawHTTP 0 Internal Server Errorwhile the same URL worked withcurl(noRange). AGETwithout the probe path (e.g.SET force_download=true) produced a normal HTTP result (e.g.401with a JSON error body), showing the bug was tied to httpfs’s HEAD / Range sequence, not the API being unreachable.Solution
In
HTTPFileHandle::LoadFileInfo():HEADreturns405 Method Not Allowedor501 Not Implemented, skip theRangeprobe and set up the existing full download path (length == 0→FullDownloadinInitialize()), which issues a plainGET.Rangeprobe fails (unexpected status) orGetRangeRequestthrowsHTTPException, fall back to that same fullGETwhenauto_fallback_to_full_downloadis true (default), preserving prior strict behavior when that setting is false.Replace the bare probe length
2with a namedconstexpr(HTTP_RANGE_FILE_SIZE_PROBE_LENGTH).Changes
src/httpfs.cpp—LoadFileInfo()branches for405/501, try/catch around the Range probe with fallback; anonymous-namespaceconstexprfor the probe size.test/sql/copy/test_remote_head_forbidden.test— clarify the test description (regression for REST-style URLs whenHEADor Range probing is unreliable).Testing
Formatting
clang-format -i src/httpfs.cppmake format-fixfrom the duckdb-httpfs tree the same way CI does.Build (local httpfs + DuckDB)
From the duckdb-httpfs repository root (with the DuckDB submodule initialized):
That produces
build/release/duckdbwith httpfs (and bundled deps such as json) linked in. AdjustVCPKG_TOOLCHAIN_PATHif your environment uses vcpkg (see the repo README).SQL logic tests
From the duckdb-httpfs root (after
make release), run the relevant sqllogictest file:That test hits a public HTTPS JSON endpoint (see the
.testfile). For a broader pass, run the full suite as in the duckdb-httpfs README:(S3/MinIO-heavy tests in this repo need Docker and extra setup per
test/README.md; the test above does not.)A/B check: upstream
httpfs.cppvs this branchTo confirm the behavior change comes from
LoadFileInfo, rebuildduckdbtwice:LoadFileInfo): restoresrc/httpfs.cppfrommain(orgit checkout main -- src/httpfs.cpp), thencmake --build build/release --target duckdb -j(or a fullmake release).Run the Manual verification SQL below for each binary. You should see HTTP 0 on the baseline and HTTP 401 on this branch (with a fake Bearer token).
Stock Python
duckdb(optional cross-check)On a release build with
INSTALL httpfs, the pre-fix symptom matches HTTP 0;SET force_download=truebefore the sameread_json_autocall typically yields a normal 401 response. After this PR ships in the extension repo, the default path should match that force_download behavior without needing the setting for this class of URL.Manual verification (upstream httpfs vs this branch)
Using a
build/release/duckdbbuilt from duckdb-httpfs with the same fake Bearer secret:Or as a one-liner:
Upstream
httpfs(pre-patchLoadFileInfo):HTTP Error: HTTP GET error on 'https://api.openai.com/v1/models' (HTTP 0 Internal Server Error)— matches the reported behavior.This branch:
HTTP Error: HTTP GET error on 'https://api.openai.com/v1/models' (HTTP 401)— a real HTTP status from OpenAI for an invalid key (same class of outcome ascurland asSET force_download=trueon stock builds), not HTTP 0.The query still errors with a fake key (expected); the fix is that the client no longer collapses to HTTP 0 during metadata / probe handling.