Skip to content

Fix query parameters being silently dropped from HTTP requests#57

Merged
dcrockwell merged 1 commit intodevelopfrom
fix/http-client-build-url-missing-query
Mar 3, 2026
Merged

Fix query parameters being silently dropped from HTTP requests#57
dcrockwell merged 1 commit intodevelopfrom
fix/http-client-build-url-missing-query

Conversation

@dcrockwell
Copy link
Copy Markdown
Contributor

Summary

  • Query parameters set via the .query() builder were silently dropped from all outgoing HTTP requests — the server never received them
  • Fixed two independent URL construction sites that both omitted the query string
  • Fixed the mock server's GET /get endpoint to actually echo query parameters (was documented but never implemented)
  • Added 8 regression tests with exact assertions covering all execution modes

Why

The .query("key=value") builder function correctly stored the query string internally, but the final step that assembled the URL for the actual HTTP request never included it. Any user relying on .query() to pass query parameters to a server was sending requests without them — with no error or warning. This affected all three ways to make a request: blocking, yielder streaming, and callback streaming.

What

Two bug fixes in the HTTP client:

The URL was assembled in two separate places in the codebase, and both had the same omission. One is used by send() and start_stream(), the other by stream_yielder(). Both now append ?query_string to the URL when query parameters are present.

One fix in the mock server:

The GET /get endpoint was documented as echoing query parameters since v1.0.0, but the implementation only used request.path — the query string was ignored. This is why no existing test could have caught the client bug. Now the endpoint includes a "query" field in its JSON response.

Eight regression tests:

All three execution modes are tested end-to-end against the mock server. Assertions parse the JSON response and do exact field matching (not substring checks). Additional tests cover URL-encoded special characters, empty query strings, and recorder record/playback round-trips with query parameters.

How

The fix itself is minimal — four lines of Gleam in each of the two URL construction sites:

let query_string = case request.query {
  option.Some(query) -> "?" <> query
  option.None -> ""
}

The query string is then concatenated onto the end of the assembled URL. When no query is set (the default), nothing changes.

Test plan

  • All 185 tests pass (177 existing + 8 new)
  • Mock server's 25 tests pass
  • Pre-commit hooks pass (format check + full build across all modules and examples)
  • send() delivers query params to server (exact JSON assertion)
  • stream_yielder() delivers query params to server
  • start_stream() delivers query params to server
  • URL-encoded special characters arrive verbatim
  • Empty query string edge case handled
  • Recorder captures query in recordings
  • Recorder playback matches by query string

## Why This Change Was Made
- The `query()` builder function let callers set query parameters on a request,
  and the value was correctly stored on the ClientRequest and copied through
  `to_http_request` — but the final URL assembly step silently dropped it.
  Every request made with `.query("key=value")` sent the request without any
  query parameters. The server never received them. This affected all three
  execution modes: `send()`, `stream_yielder()`, and `start_stream()`.

## What Was Changed
- `build_url` in `client.gleam` now appends `?query` to the URL when
  `request.query` is `Some(query)`. This fixes `send()` and `start_stream()`.
- `start_httpc_stream` in `internal.gleam` had its own independent URL
  construction that also omitted the query string — same fix applied. This
  fixes `stream_yielder()`.
- Mock server's `GET /get` endpoint now actually echoes query parameters
  (was documented since 1.0.0 but never implemented — the controller only
  passed `request.path` to the view).
- 8 regression tests added with exact JSON field assertions covering all
  three execution modes, recorder integration, URL-encoded special characters,
  and the empty query string edge case.
- Version bumped: http_client 5.1.1 → 5.1.2, mock_server 1.1.0 → 1.1.1.
- CHANGELOGs, READMEs, and release notes updated for both modules.

## Note to Future Engineer
- There are TWO separate URL construction sites: `build_url` in client.gleam
  (used by send/start_stream) and inline URL building in `start_httpc_stream`
  in internal.gleam (used by stream_yielder). If you ever add a fourth
  execution mode, you'll need to make sure it also includes query params —
  or better yet, refactor both call sites to share a single URL builder.
- The mock server's GET /get endpoint claimed to echo query params for over
  three months before anyone noticed it didn't. The CHANGELOG said it did.
  The code said otherwise. Trust the code, not the comments. Or the CHANGELOG.
  Especially not the CHANGELOG.
@dcrockwell dcrockwell merged commit ac7933c into develop Mar 3, 2026
2 checks passed
@dcrockwell dcrockwell self-assigned this Mar 3, 2026
@dcrockwell dcrockwell added bug Something isn't working module Change to a dream module labels Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working module Change to a dream module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant