Skip to main content

webfinger_rs/
actix.rs

1//! Actix Web integration for WebFinger request extraction and JRD responses.
2//!
3//! Enable the `actix` feature to:
4//!
5//! - extract [`WebFingerRequest`] from handlers mounted for `GET` requests to
6//!   [`crate::WELL_KNOWN_PATH`]; and
7//! - return [`WebFingerResponse`] directly from Actix handlers as `application/jrd+json` with the
8//!   WebFinger CORS header.
9//!
10//! The extractor reads the standard WebFinger query shape from [RFC 7033 section 4.1]:
11//!
12//! - a required `resource` query parameter; and
13//! - zero or more repeated `rel` query parameters.
14//!
15//! The `resource` value must be an absolute URI such as `acct:carol@example.com` or
16//! `https://example.com/users/carol`; relative references are rejected as malformed requests.
17//!
18//! In practice, route handlers should usually be mounted like this:
19//!
20//! ```rust
21//! use actix_web::{get, App};
22//! use webfinger_rs::{WELL_KNOWN_PATH, WebFingerRequest, WebFingerResponse};
23//!
24//! #[get("/.well-known/webfinger")]
25//! async fn webfinger(_request: WebFingerRequest) -> WebFingerResponse {
26//!     WebFingerResponse::new("acct:carol@example.com")
27//! }
28//!
29//! let app = App::new().service(webfinger);
30//! # let _ = app;
31//! # assert_eq!(WELL_KNOWN_PATH, "/.well-known/webfinger");
32//! ```
33//!
34//! The Actix router owns path and method matching. Mounting the handler with `web::get()` or
35//! `#[get]` at [`crate::WELL_KNOWN_PATH`] rejects other paths and non-`GET` methods before this
36//! extractor runs. The extractor itself validates the WebFinger request metadata available inside
37//! the handler: host, query parameters, percent encoding, and the `resource` URI.
38//!
39//! RFC 7033 requires HTTPS for WebFinger. Actix request metadata does not reliably identify the
40//! externally visible scheme when the application runs behind TLS termination or a reverse proxy, so
41//! this extractor does not enforce scheme. Configure TLS and forwarded-proto handling at your
42//! server or proxy boundary.
43//!
44//! If extraction fails, Actix returns `400 Bad Request` for missing or duplicated `resource`,
45//! missing host values, invalid percent encoding, relative resource references, or invalid resource
46//! URIs.
47//!
48//! See also [`WebFingerRequest`] for the extractor impl, [`WebFingerResponse`] for the responder
49//! impl, and the [Actix example] for a runnable server.
50//!
51//! [RFC 7033 section 4.1]: https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1
52//! [Actix example]:
53//!     https://github.com/joshka/webfinger-rs/blob/main/webfinger-rs/examples/actix.rs
54
55use std::future::{Ready, ready};
56
57use actix_web::dev::Payload;
58use actix_web::error::ErrorBadRequest;
59use actix_web::http::header::{ACCESS_CONTROL_ALLOW_ORIGIN, CONTENT_TYPE, HeaderValue};
60use actix_web::web::Json;
61use actix_web::{Error as ActixError, FromRequest, HttpRequest, HttpResponse, Responder};
62use tracing::trace;
63
64use crate::http::CORS_ALLOW_ORIGIN;
65use crate::query::{RequestParams, RequestParamsError};
66use crate::{Rel, WebFingerRequest, WebFingerResponse};
67
68const CORS_ALLOW_ORIGIN_HEADER: HeaderValue = HeaderValue::from_static(CORS_ALLOW_ORIGIN);
69const JRD_CONTENT_TYPE: HeaderValue = HeaderValue::from_static("application/jrd+json");
70
71impl Responder for WebFingerResponse {
72    /// Converts a [`WebFingerResponse`] into an Actix response.
73    ///
74    /// This serializes the body as JSON and sets the `Content-Type` header to
75    /// `application/jrd+json`, which is the JRD media type used by WebFinger.
76    /// It also sets `Access-Control-Allow-Origin: *` as recommended by RFC 7033 section 5.
77    ///
78    /// Handlers can therefore return [`WebFingerResponse`] directly without manually wrapping it in
79    /// [`actix_web::web::Json`] or setting the response header themselves.
80    ///
81    /// See also the [`crate::actix`] module docs and the [Actix example].
82    ///
83    /// # Example
84    ///
85    /// ```rust
86    /// use actix_web::{get, App};
87    /// use webfinger_rs::{Link, Rel, WebFingerRequest, WebFingerResponse};
88    ///
89    /// #[get("/.well-known/webfinger")]
90    /// async fn webfinger(request: WebFingerRequest) -> actix_web::Result<WebFingerResponse> {
91    ///     let subject = request.resource.to_string();
92    ///     let rel = Rel::new("http://webfinger.net/rel/profile-page");
93    ///     let response = if request.rels.is_empty() || request.rels.contains(&rel) {
94    ///         let link = Link::builder(rel).href("https://example.com/users/carol");
95    ///         WebFingerResponse::builder(subject).link(link).build()
96    ///     } else {
97    ///         WebFingerResponse::builder(subject).build()
98    ///     };
99    ///     Ok(response)
100    /// }
101    ///
102    /// let app = App::new().service(webfinger);
103    /// # let _ = app;
104    /// ```
105    ///
106    /// [Actix example]:
107    ///     https://github.com/joshka/webfinger-rs/blob/main/webfinger-rs/examples/actix.rs
108    type Body = <Json<WebFingerResponse> as Responder>::Body;
109
110    fn respond_to(self, request: &HttpRequest) -> HttpResponse<Self::Body> {
111        let mut response = Json(self).respond_to(request);
112        response
113            .headers_mut()
114            .insert(ACCESS_CONTROL_ALLOW_ORIGIN, CORS_ALLOW_ORIGIN_HEADER);
115        response
116            .headers_mut()
117            .insert(CONTENT_TYPE, JRD_CONTENT_TYPE);
118        response
119    }
120}
121
122impl FromRequest for WebFingerRequest {
123    /// Extracts a [`WebFingerRequest`] from an Actix request.
124    ///
125    /// The extractor reads:
126    ///
127    /// - the host from the request URI authority or the HTTP `Host` header;
128    /// - the decoded `resource` query parameter; and
129    /// - every repeated decoded `rel` query parameter.
130    ///
131    /// Query parsing percent-decodes parameters while preserving RFC 3986 query semantics.
132    ///
133    /// # Errors
134    ///
135    /// - If the request has zero or more than one `resource` query parameter, extraction returns a
136    ///   bad request.
137    /// - If the request has no URI authority and no `Host` header, extraction returns
138    ///   `ErrorBadRequest("missing host")`.
139    /// - If the query contains malformed percent encoding, extraction returns a bad request.
140    /// - If `resource` is present but cannot be parsed as a URI, extraction returns
141    ///   `ErrorBadRequest("invalid resource: ...")`.
142    ///
143    /// See also the [`crate::actix`] module docs and the [Actix example].
144    ///
145    /// # Example
146    ///
147    /// ```rust
148    /// use actix_web::{get, App};
149    /// use webfinger_rs::{WebFingerRequest, WebFingerResponse};
150    ///
151    /// #[get("/.well-known/webfinger")]
152    /// async fn webfinger(request: WebFingerRequest) -> WebFingerResponse {
153    ///     WebFingerResponse::new(request.resource.to_string())
154    /// }
155    ///
156    /// let app = App::new().service(webfinger);
157    /// # let _ = app;
158    /// ```
159    ///
160    /// [Actix example]:
161    ///     https://github.com/joshka/webfinger-rs/blob/main/webfinger-rs/examples/actix.rs
162    type Error = ActixError;
163
164    type Future = Ready<Result<Self, Self::Error>>;
165
166    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
167        trace!(?req, "extracting WebFingerRequest from request");
168        ready(extract_request(req))
169    }
170}
171
172/// Extracts WebFinger request data from Actix request metadata.
173///
174/// WebFinger request extraction only needs URI, host, and query metadata from RFC 7033 sections 4.1
175/// and 4.2, so the fallible work can stay synchronous and the Actix [`FromRequest`] implementation
176/// can wrap the result in a ready future.
177fn extract_request(req: &HttpRequest) -> Result<WebFingerRequest, ActixError> {
178    let host = req
179        .uri()
180        .host()
181        .or_else(|| req.headers().get("host").and_then(|h| h.to_str().ok()))
182        .map(|h| h.to_string())
183        .ok_or(ErrorBadRequest("missing host"))?;
184    let query: RequestParams = req.query_string().parse()?;
185    let rels = query
186        .rel
187        .into_iter()
188        .map(Rel::try_new)
189        .collect::<Result<Vec<_>, _>>()
190        .map_err(ErrorBadRequest)?;
191    Ok(WebFingerRequest {
192        host,
193        resource: query.resource,
194        rels,
195    })
196}
197
198impl From<RequestParamsError> for ActixError {
199    fn from(error: RequestParamsError) -> Self {
200        ErrorBadRequest(error)
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use actix_web::body::to_bytes;
207    use actix_web::http::StatusCode;
208    use actix_web::{App, HttpResponse, test, web};
209
210    use super::*;
211    use crate::WELL_KNOWN_PATH;
212
213    type Result<T = (), E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
214
215    /// Returns the extracted resource so tests can assert RFC 7033 query decoding behavior.
216    ///
217    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1>.
218    async fn webfinger(request: WebFingerRequest) -> HttpResponse {
219        HttpResponse::Ok().body(request.resource.to_string())
220    }
221
222    /// Returns extracted relation filters so tests can assert RFC 7033 repeated `rel` handling.
223    ///
224    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.3>.
225    async fn webfinger_rels(request: WebFingerRequest) -> HttpResponse {
226        let rels = request
227            .rels
228            .iter()
229            .map(ToString::to_string)
230            .collect::<Vec<_>>();
231        HttpResponse::Ok().json(rels)
232    }
233
234    /// Returns a minimal JRD so tests can assert responder-owned WebFinger headers.
235    async fn webfinger_response() -> WebFingerResponse {
236        WebFingerResponse::new("acct:carol@example.com")
237    }
238
239    /// Includes the RFC 7033 CORS header on successful JRD responses.
240    ///
241    /// WebFinger resources must be queryable from browsers, and RFC 7033 section 5 recommends the
242    /// least restrictive `Access-Control-Allow-Origin` value for public WebFinger resources.
243    ///
244    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-5>.
245    #[actix_web::test]
246    async fn successful_response_sets_cors_header() -> Result {
247        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_response));
248        let app = test::init_service(app).await;
249        let request = test::TestRequest::get().uri(WELL_KNOWN_PATH).to_request();
250
251        let response = test::call_service(&app, request).await;
252
253        assert_eq!(response.status(), StatusCode::OK, "{response:?}");
254        assert_eq!(
255            response.headers().get(ACCESS_CONTROL_ALLOW_ORIGIN),
256            Some(&CORS_ALLOW_ORIGIN_HEADER),
257        );
258        Ok(())
259    }
260
261    /// Returns WebFinger responses with the registered JRD media type.
262    ///
263    /// RFC 7033 section 4.2 defines `application/jrd+json` as the media type for JSON Resource
264    /// Descriptor responses.
265    ///
266    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
267    #[actix_web::test]
268    async fn webfinger_response_uses_jrd_content_type() -> Result {
269        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_response));
270        let app = test::init_service(app).await;
271        let request = test::TestRequest::get().uri(WELL_KNOWN_PATH).to_request();
272
273        let response = test::call_service(&app, request).await;
274
275        assert_eq!(response.status(), StatusCode::OK, "{response:?}");
276        assert_eq!(
277            response.headers().get(CONTENT_TYPE),
278            Some(&JRD_CONTENT_TYPE),
279        );
280        Ok(())
281    }
282
283    /// Accepts a percent-encoded `acct:` resource without panicking.
284    ///
285    /// The resource query value is percent-encoded under RFC 7033 section 4.1, then parsed as a
286    /// URI query target under RFC 7033 section 4.2.
287    ///
288    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1> and
289    /// <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
290    #[actix_web::test]
291    async fn valid_percent_encoded_resource() -> Result {
292        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
293        let app = test::init_service(app).await;
294        let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Abad%40example.org");
295        let request = test::TestRequest::get()
296            .uri(&uri)
297            .insert_header(("host", "example.org"))
298            .to_request();
299
300        let response = test::call_service(&app, request).await;
301
302        assert_eq!(response.status(), StatusCode::OK, "{response:?}");
303        let body = to_bytes(response.into_body()).await?;
304        assert_eq!(body.as_ref(), b"acct:bad@example.org");
305        Ok(())
306    }
307
308    /// Relies on Actix routing to reject non-WebFinger paths before extraction.
309    ///
310    /// RFC 7033 sections 4 and 10.1 define `/.well-known/webfinger` as the WebFinger resource.
311    /// Path matching stays in the router so applications get normal Actix `404 Not Found` behavior.
312    ///
313    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4> and
314    /// <https://www.rfc-editor.org/rfc/rfc7033.html#section-10.1>.
315    #[actix_web::test]
316    async fn wrong_path_is_not_routed() -> Result {
317        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
318        let app = test::init_service(app).await;
319        let request = test::TestRequest::get()
320            .uri("/webfinger?resource=acct%3Abad%40example.org")
321            .insert_header(("host", "example.org"))
322            .to_request();
323
324        let response = test::call_service(&app, request).await;
325
326        assert_eq!(response.status(), StatusCode::NOT_FOUND, "{response:?}");
327        Ok(())
328    }
329
330    /// Relies on Actix routing to reject non-`GET` requests before extraction.
331    ///
332    /// RFC 7033 section 4.2 specifies a `GET` request. Method matching stays in the router so
333    /// applications get normal Actix routing behavior for other methods.
334    ///
335    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
336    #[actix_web::test]
337    async fn wrong_method_is_not_routed() -> Result {
338        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
339        let app = test::init_service(app).await;
340        let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Abad%40example.org");
341        let request = test::TestRequest::post()
342            .uri(&uri)
343            .insert_header(("host", "example.org"))
344            .to_request();
345
346        let response = test::call_service(&app, request).await;
347
348        assert_eq!(response.status(), StatusCode::NOT_FOUND, "{response:?}");
349        Ok(())
350    }
351
352    /// Converts malformed resource values into Actix bad-request responses.
353    ///
354    /// RFC 7033 section 4.2 requires absent or malformed `resource` parameters to be treated as bad
355    /// requests instead of panicking inside extraction.
356    ///
357    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
358    #[actix_web::test]
359    async fn request_with_invalid_resource() -> Result {
360        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
361        let app = test::init_service(app).await;
362        let uri = format!("{WELL_KNOWN_PATH}?resource=http%3A%2F%2F%5B%3A%3A1");
363        let request = test::TestRequest::get()
364            .uri(&uri)
365            .insert_header(("host", "example.org"))
366            .to_request();
367
368        let response = test::call_service(&app, request).await;
369
370        assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
371        let body = to_bytes(response.into_body()).await?;
372        assert_eq!(body.as_ref(), b"invalid resource: invalid authority");
373        Ok(())
374    }
375
376    /// Rejects relative resource references at the Actix extractor boundary.
377    ///
378    /// RFC 7033 identifies the WebFinger query target as a URI, not a relative reference. Actix
379    /// handlers should not receive ambiguous targets such as local paths or bare names.
380    ///
381    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1> and
382    /// <https://www.rfc-editor.org/rfc/rfc3986.html#section-4.1>.
383    #[actix_web::test]
384    async fn relative_resource_is_bad_request() -> Result {
385        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
386        let app = test::init_service(app).await;
387        let uri = format!("{WELL_KNOWN_PATH}?resource=/relative");
388        let request = test::TestRequest::get()
389            .uri(&uri)
390            .insert_header(("host", "example.org"))
391            .to_request();
392
393        let response = test::call_service(&app, request).await;
394
395        assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
396        let body = to_bytes(response.into_body()).await?;
397        assert_eq!(
398            body.as_ref(),
399            b"invalid resource: resource must be an absolute URI",
400        );
401        Ok(())
402    }
403
404    /// Preserves repeated `rel` parameters instead of collapsing them.
405    ///
406    /// WebFinger clients use repeated `rel` keys to request multiple relation filters. A generic
407    /// map-shaped query parser can easily keep only one value, which would make handlers see an
408    /// incomplete request.
409    ///
410    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1>.
411    #[actix_web::test]
412    async fn valid_request_with_repeated_rel_params() -> Result {
413        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
414        let app = test::init_service(app).await;
415        let resource = "acct%3Acarol%40example.org";
416        let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=profile&rel=avatar");
417        let request = test::TestRequest::get()
418            .uri(&uri)
419            .insert_header(("host", "example.org"))
420            .to_request();
421
422        let response = test::call_service(&app, request).await;
423
424        assert_eq!(response.status(), StatusCode::OK, "{response:?}");
425        let body = to_bytes(response.into_body()).await?;
426        assert_eq!(body.as_ref(), br#"["profile","avatar"]"#);
427        Ok(())
428    }
429
430    /// Exposes decoded relation URIs to Actix handlers.
431    ///
432    /// The shared parser owns the RFC 3986 percent-decoding rule; this adapter test proves Actix
433    /// handlers receive decoded `Rel` values rather than raw query text.
434    ///
435    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1> and
436    /// <https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1>.
437    #[actix_web::test]
438    async fn rel_params_are_percent_decoded() -> Result {
439        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
440        let app = test::init_service(app).await;
441        let resource = "acct%3Acarol%40example.org";
442        let rel = "http%3A%2F%2Fwebfinger.example%2Frel%2Fprofile-page";
443        let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel={rel}");
444        let request = test::TestRequest::get()
445            .uri(&uri)
446            .insert_header(("host", "example.org"))
447            .to_request();
448
449        let response = test::call_service(&app, request).await;
450
451        assert_eq!(response.status(), StatusCode::OK, "{response:?}");
452        let body = to_bytes(response.into_body()).await?;
453        assert_eq!(
454            body.as_ref(),
455            br#"["http://webfinger.example/rel/profile-page"]"#,
456        );
457        Ok(())
458    }
459
460    /// Rejects relation values that are neither one registered relation type nor one URI.
461    ///
462    /// RFC 7033 section 4.4.4.1 allows one relation type per `rel` member. Multiple relation
463    /// filters should be encoded as repeated `rel` parameters, not as whitespace-separated values.
464    #[actix_web::test]
465    async fn invalid_rel_is_bad_request() -> Result {
466        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
467        let app = test::init_service(app).await;
468        let resource = "acct%3Acarol%40example.org";
469        let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=author%20avatar");
470        let request = test::TestRequest::get()
471            .uri(&uri)
472            .insert_header(("host", "example.org"))
473            .to_request();
474
475        let response = test::call_service(&app, request).await;
476
477        assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
478        let body = to_bytes(response.into_body()).await?;
479        assert_eq!(body.as_ref(), b"invalid relation type: author avatar");
480        Ok(())
481    }
482
483    /// Converts invalid UTF-8 after percent decoding into an Actix bad-request response.
484    ///
485    /// The shared parser owns the byte-level validation; this adapter test proves malformed
486    /// percent-encoded bytes do not reach an Actix handler as relation strings.
487    ///
488    /// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1>.
489    #[actix_web::test]
490    async fn invalid_percent_encoded_rel_is_bad_request() -> Result {
491        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
492        let app = test::init_service(app).await;
493        let resource = "acct%3Acarol%40example.org";
494        let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=%FF");
495        let request = test::TestRequest::get()
496            .uri(&uri)
497            .insert_header(("host", "example.org"))
498            .to_request();
499
500        let response = test::call_service(&app, request).await;
501
502        assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
503        let body = to_bytes(response.into_body()).await?;
504        assert_eq!(body.as_ref(), b"invalid percent-encoded query parameter");
505        Ok(())
506    }
507
508    /// Rejects malformed percent escape syntax instead of treating `%` literally.
509    ///
510    /// The shared query parser owns the RFC 3986 check; this Actix test proves that parser errors are
511    /// converted into `400 Bad Request` responses instead of escaping the extractor boundary.
512    ///
513    /// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-2.1>.
514    #[actix_web::test]
515    async fn malformed_percent_escape_is_bad_request() -> Result {
516        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
517        let app = test::init_service(app).await;
518        let resource = "acct%3Acarol%40example.org";
519        let uri = format!("{WELL_KNOWN_PATH}?resource={resource}&rel=%GG");
520        let request = test::TestRequest::get()
521            .uri(&uri)
522            .insert_header(("host", "example.org"))
523            .to_request();
524
525        let response = test::call_service(&app, request).await;
526
527        assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
528        let body = to_bytes(response.into_body()).await?;
529        assert_eq!(body.as_ref(), b"invalid percent-encoded query parameter");
530        Ok(())
531    }
532
533    /// Accepts `resource` in any query parameter position through the Actix extractor.
534    ///
535    /// RFC 7033 section 4.1 does not make parameter order significant. This adapter test proves
536    /// Actix handlers still receive relation filters when `resource` appears after them.
537    ///
538    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1>.
539    #[actix_web::test]
540    async fn resource_parameter_order_does_not_matter() -> Result {
541        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger_rels));
542        let app = test::init_service(app).await;
543        let resource = "acct%3Acarol%40example.org";
544        let uri = format!("{WELL_KNOWN_PATH}?rel=profile&resource={resource}");
545        let request = test::TestRequest::get()
546            .uri(&uri)
547            .insert_header(("host", "example.org"))
548            .to_request();
549
550        let response = test::call_service(&app, request).await;
551
552        assert_eq!(response.status(), StatusCode::OK, "{response:?}");
553        let body = to_bytes(response.into_body()).await?;
554        assert_eq!(body.as_ref(), br#"["profile"]"#);
555        Ok(())
556    }
557
558    /// Keeps encoded `=` and `&` inside handler-visible resource values.
559    ///
560    /// Resource URIs may contain query strings of their own. This adapter test proves Actix receives
561    /// the decoded target resource without splitting encoded inner delimiters into WebFinger
562    /// parameters.
563    ///
564    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.1>.
565    #[actix_web::test]
566    async fn encoded_delimiters_stay_inside_resource() -> Result {
567        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
568        let app = test::init_service(app).await;
569        let resource = "https%3A%2F%2Fexample.org%2Fprofile%3Fa%3D1%26b%3D2";
570        let uri = format!("{WELL_KNOWN_PATH}?resource={resource}");
571        let request = test::TestRequest::get()
572            .uri(&uri)
573            .insert_header(("host", "example.org"))
574            .to_request();
575
576        let response = test::call_service(&app, request).await;
577
578        assert_eq!(response.status(), StatusCode::OK, "{response:?}");
579        let body = to_bytes(response.into_body()).await?;
580        assert_eq!(body.as_ref(), b"https://example.org/profile?a=1&b=2");
581        Ok(())
582    }
583
584    /// Preserves literal `+` in Actix handler-visible resources.
585    ///
586    /// Actix's normal query extractor is not used here because WebFinger follows RFC 3986 query
587    /// semantics, where `+` remains data instead of becoming a space.
588    ///
589    /// See <https://www.rfc-editor.org/rfc/rfc3986.html#section-3.4>.
590    #[actix_web::test]
591    async fn plus_is_not_decoded_as_space() -> Result {
592        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
593        let app = test::init_service(app).await;
594        let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Acarol+tag%40example.org");
595        let request = test::TestRequest::get()
596            .uri(&uri)
597            .insert_header(("host", "example.org"))
598            .to_request();
599
600        let response = test::call_service(&app, request).await;
601
602        assert_eq!(response.status(), StatusCode::OK, "{response:?}");
603        let body = to_bytes(response.into_body()).await?;
604        assert_eq!(body.as_ref(), b"acct:carol+tag@example.org");
605        Ok(())
606    }
607
608    /// Rejects duplicate `resource` parameters at the Actix extractor boundary.
609    ///
610    /// The parser owns the RFC 7033 section 4.2 rule that there is exactly one target. This adapter
611    /// test proves ambiguous requests become `400 Bad Request` responses rather than arbitrary
612    /// handler inputs.
613    ///
614    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
615    #[actix_web::test]
616    async fn request_with_multiple_resources() -> Result {
617        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
618        let app = test::init_service(app).await;
619        let carol = "acct%3Acarol%40example.org";
620        let alice = "acct%3Aalice%40example.org";
621        let uri = format!("{WELL_KNOWN_PATH}?resource={carol}&resource={alice}");
622        let request = test::TestRequest::get()
623            .uri(&uri)
624            .insert_header(("host", "example.org"))
625            .to_request();
626
627        let response = test::call_service(&app, request).await;
628
629        assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
630        let body = to_bytes(response.into_body()).await?;
631        assert_eq!(body.as_ref(), b"multiple resource parameters");
632        Ok(())
633    }
634
635    /// Rejects requests that omit the required `resource` parameter.
636    ///
637    /// The shared query parser owns the exact RFC 7033 rule; this Actix test proves that missing
638    /// `resource` is exposed as an Actix bad-request response.
639    ///
640    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4.2>.
641    #[actix_web::test]
642    async fn request_with_missing_resource() -> Result {
643        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
644        let app = test::init_service(app).await;
645        let request = test::TestRequest::get()
646            .uri(WELL_KNOWN_PATH)
647            .insert_header(("host", "example.org"))
648            .to_request();
649
650        let response = test::call_service(&app, request).await;
651
652        assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
653        let body = to_bytes(response.into_body()).await?;
654        assert_eq!(body.as_ref(), b"missing resource parameter");
655        Ok(())
656    }
657
658    /// Rejects requests where neither the URI nor `Host` header provides an authority.
659    ///
660    /// The request host is significant to WebFinger query routing.
661    ///
662    /// See <https://www.rfc-editor.org/rfc/rfc7033.html#section-4>.
663    #[actix_web::test]
664    async fn request_with_no_host() -> Result {
665        let app = App::new().route(WELL_KNOWN_PATH, web::get().to(webfinger));
666        let app = test::init_service(app).await;
667        let uri = format!("{WELL_KNOWN_PATH}?resource=acct%3Abad%40example.org");
668        let request = test::TestRequest::get().uri(&uri).to_request();
669
670        let response = test::call_service(&app, request).await;
671
672        assert_eq!(response.status(), StatusCode::BAD_REQUEST, "{response:?}");
673        let body = to_bytes(response.into_body()).await?;
674        assert_eq!(body.as_ref(), b"missing host");
675        Ok(())
676    }
677}