feat: declarative #[http_client] HTTP-interface client (26.6.28)#30
Merged
Conversation
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
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>
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.
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>Implthat issues the requests over aWebClient— the mirror image of a controller.#[get("/path")]/#[post]/#[put]/#[delete]/#[patch]+ generic#[request(method=…)]. Path vars use the framework's:idsyntax (same as the server macro);{id}is a compile error.:var→ path variable, lone non-scalar arg on a body verb → JSON body, the rest → query params (Optionomits,Vecrepeats). Override with#[path]/#[query("k")]/#[header("X")]/#[body]. Every:varmust bind exactly once.async fn -> Result<T, ClientError>(default),Result<T, E: From<ClientError>>, non-asyncMono<T>/Flux<T>,WebClientResponseescape hatch.#[http_client(… bean)]registers<Trait>Implas a@Service+ bindsdyn Trait, so#[autowired] Arc<dyn Trait>resolves (the Feign-client payoff).Process
:idpath syntax,<Trait>Implnaming) confirmed by the maintainer.Option/Vecpath variable compiled and produced…/Some(x)/…/NoneURLs; it's now a compile error. Also fixed: custom-method body-inference + a runtime panic on a malformed#[request(method=…)], an object-safety gap underbean, and macro hygiene (mixed_site()for generated locals).Tests
a/b→a%2Fb),Optionquery omission, header, JSON body,Vecdecode,204→(),404→ClientError::Problem.is_not_found(), NDJSONFlux, empty-body→Ok(None)fold, custom-errormap_err, and DIdyn Traitresolution.{id}syntax, two verbs, double#[body], ambiguous body, unbound:var,Optionpath var, missing&self, async-Mono, assoc-type underbean,#[request]missing method, …).#[rest_controller]verb grammar (MappingAttr/VERBS/join_path→pub(crate), no code moved) so client and server can't drift.make cigreen: 317 suites, 4503 tests, 0 failed. Book: a "declarative client" section in13-http-clients.md.Follows the rollback-rules increment (#29) on the parity roadmap.