Skip to content

Migrate to Axum [part 2]: Migrate hpke_config to Axum#4427

Merged
jcjones merged 4 commits intomainfrom
4283-migrate-to-axum-part-2
Mar 13, 2026
Merged

Migrate to Axum [part 2]: Migrate hpke_config to Axum#4427
jcjones merged 4 commits intomainfrom
4283-migrate-to-axum-part-2

Conversation

@jcjones
Copy link
Copy Markdown
Contributor

@jcjones jcjones commented Mar 5, 2026

  • This adds an axum_hpke_config handler function, wiring it into Axum.
  • It leaves behind the existing hpke_config and hpke_config_cors_preflight methods to support other unit tests that want them for now, but we'll clean that up in part 6.
  • Strips out part 1's demo test, and instead routes "/hpke_config" via axum::routing::get(axum_hpke_config::<C>).layer(hpke_cors), roughly like I do in Relay.
  • Pulled impl Error and into_response_with_retry_after out of Migrate to Axum #4409.

Partly authored by Claude

@jcjones jcjones added this to the 2026Q1 Janus Release milestone Mar 5, 2026
@jcjones jcjones marked this pull request as ready for review March 5, 2026 17:39
@jcjones jcjones requested a review from a team as a code owner March 5, 2026 17:39
@jcjones jcjones force-pushed the 4283-migrate-to-axum-part-2 branch from 1064aed to 81087b8 Compare March 5, 2026 21:40
Ok((StatusCode::OK, response_headers, encoded_hpke_config_list).into_response())
}

// TODO(#4283): Remove in Part 6 when Trillium is fully removed.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not delete it (and hpke_config_cors_preflight) now?


if let Some(signature) = signature {
response_headers.insert(
http::header::HeaderName::from_static(HPKE_CONFIG_SIGNATURE_HEADER),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Odd that we use http::header::HeaderValue but fully quality http::header::HeaderName here

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a Claudeism and it snuck through me, sorry about that. Haste makes LLM slop.


#[tokio::test]
async fn test_retry_after_header() {
use axum::response::IntoResponse;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Put this use at the top of the module.

}
}

impl IntoResponse for &ProblemDocument<'_> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you implement this for ProblemDocument then you won't have to awkwardly do (&doc).into_response() all over Error::to_response

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦🏼 yeah!

}

/// Converts this problem document into an axum [`Response`].
pub fn into_response_with_retry_after(&self, retry_after: Option<Duration>) -> Response {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally, naming something into implies that you move the value in ("A value-to-value conversion that consumes the input value."). It doesn't look like this moves any part of self into the returned value.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:( sorry. It's now to_response_with_retry_after

.with_task_id(&inner.task_id)
.with_detail(&inner.to_string()))
.into_response(),
Error::Datastore(error @ datastoreError::TimeUnaligned { task_id, .. }) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it's orthogonal to this PR, but is this error possible anymore? #4430

@jcjones jcjones force-pushed the 4283-migrate-to-axum-part-2 branch from 81087b8 to 4ad0c58 Compare March 9, 2026 19:15
@jcjones jcjones requested a review from tgeoghegan March 9, 2026 20:55
Base automatically changed from 4283-migrate-to-axum-part-1 to main March 10, 2026 22:34
jcjones added 3 commits March 10, 2026 15:43
- This adds an `axum_hpke_config` handler function, wiring it into Axum.
- It leaves behind the existing `hpke_config` and `hpke_config_cors_preflight` methods to support other unit tests that want them for now, but we'll clean that up in part 6.
- Strips out part 1's demo test, and instead routes `"/hpke_config"` via `axum::routing::get(axum_hpke_config::<C>).layer(hpke_cors)`, roughly like I do in Relay.
- Pulled `impl Error` and `into_response_with_retry_after` out of #4409.

Partly authored by Claude
@jcjones jcjones force-pushed the 4283-migrate-to-axum-part-2 branch from 4ad0c58 to 32c0934 Compare March 10, 2026 22:52
Copy link
Copy Markdown
Contributor

@tgeoghegan tgeoghegan left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's nice that this doesn't penetrate deeper into the call stack below aggregator.handle_hpke_config(). 🤞🏻 that the rest of our endpoints are similarly well-layered.


/// Converts this problem document into an axum [`Response`].
pub fn to_response_with_retry_after(&self, retry_after: Option<Duration>) -> Response {
let body = serde_json::to_vec(self).unwrap_or_default();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a nit: should we be suppressing failures to encode problem documents as JSON? Default here will be an empty Vec and then we'll send out a malformed response that has Content-Type: application/json+problem_document (or whatever it exactly is) and no body, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, this calls for warn!. Let's try:

let body = match serde_json::to_vec(self) {
        Ok(body) => body,
        Err(e) => {
            warn!(?e, ?self, "Failed to serialize problem document");
            "{}".as_bytes().to_vec()
         }
};


[dev-dependencies]
janus_aggregator = { workspace = true, features = ["fpvec_bounded_l2", "test-util"] }
tower.workspace = true
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have tower = { workspace = true } above in regular dependencies and this doesn't add any features. I think we can delete it?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rebase fail, thanks!

/// are tightly scoped to relevant endpoints, and our CORS settings are unlikely to change.
/// See: <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age>.
const CORS_PREFLIGHT_CACHE_AGE: u32 = 24 * 60 * 60;
const CORS_PREFLIGHT_CACHE_AGE: StdDuration = StdDuration::from_secs(24 * 60 * 60);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm 99.9% this is good and right (it's definitely better than a u32) but just so we thought about it: in other places like the datastore, we've moved toward using chrono types like TimeDelta. If we did that here, would that just cause friction when we try to stick this value into HTTP headers or something-something-CORS?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used StdDuration because it's already used all over this file, but hmm!

No, ugh:

the trait `From<chrono::TimeDelta>` is not implemented for `tower_http::cors::MaxAge`

No feature for chrono types either: https://docs.rs/crate/tower-http/latest/features

@jcjones jcjones merged commit d5c7df8 into main Mar 13, 2026
8 checks passed
@jcjones jcjones deleted the 4283-migrate-to-axum-part-2 branch March 13, 2026 18:23
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.

3 participants