Skip to content

feat: declarative #[http_client] HTTP-interface client (26.6.28)#30

Merged
ancongui merged 1 commit into
mainfrom
feat/http-client-declarative
Jun 16, 2026
Merged

feat: declarative #[http_client] HTTP-interface client (26.6.28)#30
ancongui merged 1 commit into
mainfrom
feat/http-client-declarative

Conversation

@ancongui

Copy link
Copy Markdown
Contributor

What

A Spring Boot parity increment (26.6.28): the declarative HTTP-interface client — the highest single value lever from the 16-area parity-gap analysis (it lifts the REST/HTTP-clients area, the lowest at ~60%, off the floor).

#[http_client] is the analog of Spring 6's @HttpExchange (the modern OpenFeign replacement). Annotate a trait of methods with the same verb attributes a #[rest_controller] uses, and the macro generates a <Trait>Impl that issues the requests over a WebClient — the mirror image of a controller.

#[http_client(path = "/api/v1/orders", name = "orders", bean)]
pub trait OrdersClient {
    #[get("/:id")]
    async fn get_order(&self, id: String) -> Result<Order, ClientError>;
    #[get("/")]
    async fn list(&self, status: String, page: Option<u32>) -> Result<Vec<Order>, ClientError>;
    #[post("/")]
    async fn create(&self, #[header("X-Tenant")] tenant: String, order: CreateOrder) -> Result<Order, ClientError>;
    #[delete("/:id")]
    async fn cancel(&self, id: String) -> Result<(), ClientError>;   // 204 -> ()
    #[get("/stream")]
    fn stream(&self) -> Flux<Order>;
}
let api = OrdersClientImpl::new("https://orders.svc");
  • Verbs: #[get("/path")]/#[post]/#[put]/#[delete]/#[patch] + generic #[request(method=…)]. Path vars use the framework's :id syntax (same as the server macro); {id} is a compile error.
  • Binding needs no attributes in the common case: name-matched :var → path variable, lone non-scalar arg on a body verb → JSON body, the rest → query params (Option omits, Vec repeats). Override with #[path]/#[query("k")]/#[header("X")]/#[body]. Every :var must bind exactly once.
  • Returns: async fn -> Result<T, ClientError> (default), Result<T, E: From<ClientError>>, non-async Mono<T>/Flux<T>, WebClientResponse escape hatch.
  • DI: #[http_client(… bean)] registers <Trait>Impl as a @Service + binds dyn Trait, so #[autowired] Arc<dyn Trait> resolves (the Feign-client payoff).

Process

  • Designed via a scored 3-proposal judge panel (Spring-faithful / firefly-consistent / ergonomic-minimal anchors) synthesized into one spec; two flagged API decisions (:id path syntax, <Trait>Impl naming) confirmed by the maintainer.
  • Adversarially reviewed — the review caught a real runtime footgun: an Option/Vec path variable compiled and produced …/Some(x) / …/None URLs; it's now a compile error. Also fixed: custom-method body-inference + a runtime panic on a malformed #[request(method=…)], an object-safety gap under bean, and macro hygiene (mixed_site() for generated locals).

Tests

  • In-process axum round-trip (real wire behavior): path-var encode (a/ba%2Fb), Option query omission, header, JSON body, Vec decode, 204(), 404ClientError::Problem.is_not_found(), NDJSON Flux, empty-body→Ok(None) fold, custom-error map_err, and DI dyn Trait resolution.
  • 15 trybuild compile-fail cases locking clean diagnostics ({id} syntax, two verbs, double #[body], ambiguous body, unbound :var, Option path var, missing &self, async-Mono, assoc-type under bean, #[request] missing method, …).
  • Reuses the server #[rest_controller] verb grammar (MappingAttr/VERBS/join_pathpub(crate), no code moved) so client and server can't drift.

make ci green: 317 suites, 4503 tests, 0 failed. Book: a "declarative client" section in 13-http-clients.md.

Follows the rollback-rules increment (#29) on the parity roadmap.

A Spring Boot parity increment — the highest single value lever from the
16-area parity-gap analysis (it lifts the REST/HTTP-clients area off the
floor). Designed via a scored 3-proposal judge panel and adversarially
reviewed before merge.

#[http_client] — the analog of Spring 6's @HttpExchange (modern OpenFeign
replacement). Annotate a trait of methods with the SAME verb attributes a
#[rest_controller] uses; the macro generates a <Trait>Impl that issues the
requests over a WebClient — the mirror image of a controller.
  - Verbs: #[get("/path")]/#[post]/#[put]/#[delete]/#[patch] + generic
    #[request(method="...")]. Path vars use the framework's :id syntax (same
    as the server macro); {id} is a compile error pointing at :id.
  - Argument binding needs no attributes in the common case: a name-matched
    :var arg is the path variable, the lone non-scalar arg on a body verb is
    the JSON body, the rest are query params (Option omits when None, Vec/&[_]
    repeat). Override with #[path]/#[query("k")]/#[header("X")]/#[body]. Every
    :var must bind exactly once or it's a compile error; an Option/Vec/slice
    path variable is rejected (caught by review — it produced .../Some(x) URLs).
  - Return shapes: async fn -> Result<T, ClientError> (ergonomic default),
    Result<T, E: From<ClientError>>, non-async Mono<T>/Flux<T> (returned
    directly; Flux defaults Accept: application/x-ndjson), WebClientResponse
    (.exchange() escape hatch).
  - Construction: <Trait>Impl::new(base_url) or ::with_client(WebClient); Clone.
    With #[http_client(... bean)] it's registered as a @service and bound to
    dyn Trait, so #[autowired] Arc<dyn Trait> resolves (sharing a WebClient bean,
    named via client = "...").
  - Error fidelity (documented): awaited Result<T, ClientError> surfaces every
    failure as ClientError::Problem (FireflyError with original status/code, so
    is_not_found()/is_server_error()/is_retryable() still classify); structured
    Transport/Decode/Encode/InvalidUrl variants survive only on Mono/Flux.

Reuses the server #[rest_controller] verb grammar (MappingAttr/VERBS/join_path
made pub(crate); no code moved) so client and server can't drift. Added
firefly_client::encode_path_segment (RFC 3986 path-segment encoding). The
firefly::prelude now also re-exports WebClient/ClientError/new_web_client.

Tests: in-process axum round-trip (path-var encode, Option-query omit, header,
JSON body, Vec decode, 204->(), 404->Problem.is_not_found(), NDJSON Flux,
Option empty-body fold, custom-error map_err, DI dyn-Trait resolve) + 12
trybuild compile-fail cases. make ci green: 317 suites, 4503 tests, 0 failed.
Book: docs/book/src/13-http-clients.md "The declarative client" section.
@ancongui ancongui merged commit ec2ef67 into main Jun 16, 2026
4 checks passed
@ancongui ancongui deleted the feat/http-client-declarative branch June 16, 2026 17:56
ancongui added a commit that referenced this pull request Jun 16, 2026
#31)

The published dist (firefly-rust-by-example.{pdf,epub}) was stale — built
before the README/book audits (#26/#27) and the new framework features
(declarative rollback rules #29, #[http_client] #30). Regenerated against
current content and completed the manifest.

- book.yaml: added the three chapters that existed in src/ but were omitted
  from the curated PDF manifest, renumbered the book contiguously 1–26:
  - ch6  "Application Bootstrap"        (04b-bootstrap.md, Part I)
  - ch9  "OpenAPI & API Documentation"  (06a-openapi.md, Part II)
  - ch18 "Layered Microservices"        (22-layered-microservices.md, Part IV)
  (introduction.md stays excluded — the PDF has its own front matter.)
- gen_openers.py: three new on-brand 720x300 chapter openers (s_bootstrap,
  s_openapi, s_layered) in the established style; switched the kicker numbers
  to an explicit reading-order list so every "CHAPTER N" matches book.yaml.
  The 18 ch06–ch23 openers changed only because their kicker number shifted;
  ch01–ch05 are byte-identical.
- dist: regenerated PDF (485 -> 519 pages, +3 chapters) and EPUB. The new
  #[http_client] (ch13) and #[transactional] rollback-rules (ch8) sections
  now render in the published book.

Built with the WeasyPrint pipeline (docs/book/build-book.sh; venv +
weasyprint 69). Docs-only — no crate or version change.

Co-authored-by: Andrés Contreras Guillén <ancongui@Andress-MacBook-Pro-2.local>
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.

1 participant