Skip to content

Accept CONNECT and absolute-form request-targets (RFC 9112 §3.2) - #923

Open
CodyPubNub wants to merge 3 commits into
cloudflare:mainfrom
CodyPubNub:connect-absolute-form-parsing
Open

Accept CONNECT and absolute-form request-targets (RFC 9112 §3.2)#923
CodyPubNub wants to merge 3 commits into
cloudflare:mainfrom
CodyPubNub:connect-absolute-form-parsing

Conversation

@CodyPubNub

@CodyPubNub CodyPubNub commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Resolves #909. Alternative to #912.

Summary

  • Accept absolute-form request targets such as GET http://host/path.
  • Accept authority-form only for valid CONNECT host:port targets.
  • Preserve scheme and authority on the parsed Uri, while forwarding
    absolute-form to an origin as origin-form.
  • Keep RequestHeader unchanged when set_raw_path() rejects a replacement.
  • Return / for non-CONNECT URIs without a path-and-query, and limit the
    authority fallback to CONNECT.

Why

RequestHeader::set_raw_path() previously sent every request target through
Uri::builder().path_and_query(). Starting with http 1.4.1, that builder
rejects targets other than origin-form or *. This makes valid HTTP/1
absolute-form and CONNECT authority-form requests fail before application code
runs.

RFC 9112 §3.2.2 requires servers to accept absolute-form. RFC 9112 §3.2.3 defines CONNECT authority-form as uri-host ":" port.

Behavior

Request target Stored path Stored authority raw_path() / origin wire
/path?q /path /path?q
* * *
http://host/path?q /path host /path?q
http://host / host /
http://host?q / host /?q
host:443 with CONNECT "" host:443 host:443

Malformed CONNECT targets are rejected, including targets with a scheme, path,
query, userinfo, missing or invalid port, empty host, malformed IP-literal, or
invalid reg-name characters.

Implementation

  • parse_request_target() computes the new Uri and raw fallback before
    updating either field, so errors leave the existing header intact.
  • Absolute-form is parsed as a full Uri. Scheme and authority remain
    available to callers; the stored path-and-query is normalized for later H1
    or H2 forwarding.
  • CONNECT targets retain their original bytes for authority-form
    serialization and are validated as uri-host ":" port.
  • Bracketed hosts accept IPv6 and the RFC 3986 IPvFuture grammar. Unbracketed
    hosts accept the http::Uri-representable subset of reg-name.
  • Origin-form and asterisk-form retain their existing path-and-query parsing.

No new fields are added. The common origin-form path does not perform
authority validation.

Interaction with current main

0c08149 independently fixed the same raw_path() panic by falling back to the Uri authority unconditionally. This branch retains that fallback for CONNECT and intentionally narrows it for other methods, which return / instead. Authority-form is valid only for CONNECT (§3.2.3); origin-form requires at least / (§3.2.1). The upstream test_authority_form_raw_path test is kept and passes unchanged.

Tests

  • pingora-http unit tests cover all four request-target forms, malformed
    absolute and authority targets, IPv6, IPvFuture, reg-name, and atomic failure.
  • pingora-core tests drive absolute-form and CONNECT through the real stock
    HTTP/1 downstream parser using mock IO, including a malformed
    CONNECT [gg]:443 that is rejected at read time — validation fires on actual
    inbound H1 traffic, not just on constructed headers.
  • H1 request-line serialization is checked byte-for-byte for absolute-form and
    CONNECT.
  • Existing allowed and disallowed H1 CONNECT proxy tests now run without the
    patched_http1 feature.
  • A proxy integration test sends absolute-form inbound and verifies the origin
    receives GET /absolute?q=1 HTTP/1.1.

Local verification:

  • cargo test -p pingora-http --lib: 52 passed
  • cargo test -p pingora-core --lib request_target: 4 passed
  • cargo test -p pingora-proxy --lib: 22 passed
  • cargo test -p pingora-proxy --test test_basic --no-run: clean
  • cargo check --workspace: clean
  • cargo fmt --all -- --check: clean
  • git diff --check: clean

The three CONNECT/absolute-form proxy integrations compile locally but require the external OpenResty fixture and have not run locally. Stock-parser behavior is covered locally by the pingora-core mock-IO tests; the integrations will execute in CI.

Difference from #912

  • Absolute-form without a path is normalized to /, rather than re-emitting
    the absolute URI as the origin request target.
  • http://host?q is normalized to /?q on the stored Uri, so a later H2
    :path rebuild is valid.
  • CONNECT validation rejects shapes that http::Uri accepts but RFC 9112
    authority-form does not.

Out of scope

  • Replacing Host from the absolute-form authority.
  • Preserving absolute-form when forwarding to another proxy rather than an
    origin.
  • Rewriting server-wide OPTIONS absolute-form to *.
  • Non-UTF-8 targets without a leading slash.
  • Percent-encoded reg-names, which http::Uri rejects before host validation.

As of http 1.4.1, PathAndQuery rejects any input that is not origin-form
or "*" (RFC 9112 §3.2), so set_raw_path() — which routed every target
through Uri::builder().path_and_query() — fails to parse CONNECT and
absolute-form request-targets. pingora-http declares http = "1", so a
routine dependency bump silently breaks every forward-proxy and CONNECT
request. (Before 1.4.1 the same call mis-parsed absolute-form, folding
scheme+authority into the path so route matching failed — broken either
way.)

- absolute-form ("http://host/path", §3.2.2): a server MUST accept it.
  Parse as a full Uri, keeping scheme+authority available to callers,
  and re-serialize origin-form on the wire (raw_path() derives from
  path_and_query()). An empty path is "/" (§3.2.1); the no-path-with-
  query shape ("http://host?q") is canonicalized to "/?q" on the stored
  Uri so a later rebuild (e.g. the H1->H2 :path) stays valid.
- authority-form ("host:443", §3.2.3): accepted only for CONNECT and
  only as uri-host ":" port — a scheme, path, query, missing port, or
  userinfo are rejected. The target is re-serialized verbatim so the
  existing CONNECT tunneling path (allow_connect_method_proxying) runs.
- raw_path() no longer unwraps path_and_query(), and set_raw_path()
  clears any stale fallback on entry so a reused header cannot serialize
  a previous target.

Origin-form and asterisk-form keep their fast path; malformed targets
still error. The non-UTF-8 lossy branch is untouched.

Verified by request-target unit tests and end-to-end re-serialization
through http_req_header_to_wire().

Resolves cloudflare#909
Resolve conflict in pingora-http/src/lib.rs:

- raw_path(): main's 0c08149 independently added an authority fallback for the
  same panic this branch fixes. Keep the branch version, which is the stricter
  superset: it gates the authority fallback on CONNECT (RFC 9112 §3.2.3 admits
  authority-form only there) and returns "/" rather than empty for a Uri with
  no path-and-query, since origin-form requires at least "/" (§3.2.1). Main's
  test_authority_form_raw_path is kept and passes against it.
- Drop the DerefMut import removed by main's 4d7061b, keeping the Error import
  this branch's request-target validation needs.
- Both test blocks are additive; keep all of them.
- Parse request targets before updating RequestHeader so a rejected mutation
  cannot discard the existing raw CONNECT or non-UTF-8 target.
- Validate CONNECT uri-host syntax, including IPv6 and IPvFuture, because
  http::Uri accepts malformed bracketed authorities.
- Exercise absolute-form and authority-form through the stock H1 parser, and
  run the existing CONNECT proxy tests without patched_http1.
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.

HTTP/1 CONNECT and absolute-form requests fail to parse with http crate >= 1.4.1 (set_raw_path rejects non-origin-form request targets)

1 participant