Skip to content

Make Server::into_router() public for TLS support#12

Open
ivonnyssen wants to merge 1 commit intoRReverser:mainfrom
ivonnyssen:pr/expose-into-router
Open

Make Server::into_router() public for TLS support#12
ivonnyssen wants to merge 1 commit intoRReverser:mainfrom
ivonnyssen:pr/expose-into-router

Conversation

@ivonnyssen
Copy link
Copy Markdown
Contributor

Summary

Changes Server::into_router() from fn to pub fn, allowing consumers to obtain the underlying axum::Router and handle socket binding and TLS wrapping themselves.

Motivation

The current API only exposes Server::listen() and Server::bind(), both of which bind to a plain TCP socket internally. For TLS termination at the Alpaca server (e.g. using axum-server with rustls), consumers need access to the router so they can wrap it in their own axum_server::bind_rustls() call without duplicating the entire route setup.

Changes

  • src/server/mod.rs: Changed fn into_router(self) -> Router to pub fn into_router(self) -> Router and added documentation explaining the use case. The caller is responsible for starting the discovery server separately if needed.

Test plan

  • cargo clippy passes (full CI feature matrix verified)
  • Existing server tests continue to pass
  • No breaking changes — this only widens visibility

@ivonnyssen ivonnyssen marked this pull request as ready for review April 12, 2026 23:43
Copilot AI review requested due to automatic review settings April 12, 2026 23:44
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR widens the server API to support custom socket binding (including TLS termination) by exposing the internal Axum router so downstream consumers can run the server with their own listener/acceptor setup.

Changes:

  • Made Server::into_router(self) -> Router public.
  • Added rustdoc to explain the intended TLS/binding use case and discovery-server responsibility.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/server/mod.rs Outdated
Comment on lines 237 to 244
/// Consumes the server and returns the underlying [`axum::Router`].
///
/// Use this when you need to handle socket binding and TLS wrapping
/// yourself instead of using [`Self::bind`]. The caller is responsible
/// for starting the Alpaca discovery server separately if needed.
#[expect(clippy::too_many_lines)]
fn into_router(self) -> Router {
pub fn into_router(self) -> Router {
let devices = Arc::new(self.devices);
Copy link

Copilot AI Apr 12, 2026

Choose a reason for hiding this comment

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

Making into_router public exposes axum::Router in this crate’s public API surface. That effectively couples semver compatibility to Axum’s Router type (e.g., an Axum major upgrade could become a breaking change for this crate even if internal usage stays the same). If you want to keep the public API more stable, consider returning a crate-defined wrapper/type alias (and/or gating this behind a feature) so downstream code doesn’t need to name axum::Router directly.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Owner

@RReverser RReverser left a comment

Choose a reason for hiding this comment

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

I actually have same concern as Copilot surfaced here. I intentionally tried to keep axum an implementation detail - it should be possible to switch server implementations if we decide to, without worrying about public API.

Is there a smaller-scoped change that could be made to still provide the desired functionality? At the very least changing the return signature to impl tower_service::Service would already limit the API surface.

@ivonnyssen
Copy link
Copy Markdown
Contributor Author

Thanks for the review. I'd like to take this further than a direct signature swap so the decoupling is structural rather than nominal.

Quick note on why I don't think impl tower_service::Service alone gets us there: axum::Router::call takes http::Request<axum::body::Body> and produces http::Response<axum::body::Body>, so even behind impl Service<...> those associated types still carry axum::body::Body. To actually achieve the "swap server implementation freely" goal, the body types need to be owned at the boundary too.

With that in mind, three shapes I've considered. I think A is the best fit; listing B and C for completeness since I don't think either is better than A.

A (recommended): opaque service newtype

pub struct AlpacaService { /* private — wraps the internal router */ }
impl Clone for AlpacaService { ... }

impl<B> tower_service::Service<http::Request<B>> for AlpacaService
where
    B: http_body::Body<Data = Bytes> + Send + 'static,
    B::Error: Into<BoxError>,
{
    type Response = http::Response<UnsyncBoxBody<Bytes, BoxError>>;
    type Error = Infallible;
    type Future = BoxFuture<'static, Result<Self::Response, Infallible>>;
}

impl Server {
    pub fn into_service(self) -> AlpacaService { ... }
}

Body conversion stays inside call() — inbound body wrapped into axum::body::Body, outbound boxed on the way out. Public signature: only http, http-body, http-body-util, bytes, tower-service. Fits tower::ServiceBuilder::layer(...), hyper_util::server::conn::auto, axum_server::bind_rustls(...) via Shared, etc. Cost is ~40 lines of wrapping plus one body map each direction per request.

B: runner-style API, service never leaves the crate

impl Server {
    pub async fn serve_on(self, listener: TcpListener) -> Result<Infallible>;
    pub async fn serve_tls(self, listener: TcpListener, cfg: rustls::ServerConfig) -> Result<Infallible>;
}

Also hides axum, simpler than A, but rigid — each new integration needs a new upstream method, and middleware composition isn't covered out of the box.

Concrete example: a downstream service I maintain wraps the Alpaca routes with HTTP Basic Auth middleware (config-driven Authorization: Basic validation against a hashed password, 401 otherwise) before serving over TLS. Today that's router.layer(middleware::from_fn_with_state(auth_cfg, ...)) composed on top of Server::into_router(), handed to a tower-based TLS serve loop. Option B would need a new upstream method like serve_tls_with_layer<L: tower::Layer<???>>(...) to support it — and the awkward bit is the layer's target type, which has to be expressive enough for real middleware (per-request state, async, response rewriting) without re-exposing an axum-specific middleware contract.

C: callback-style binding

pub fn bind_with<F, Out>(self, build: F) -> Out
where F: FnOnce(AlpacaService) -> Out;

Hands the service to a user closure and serves whatever comes back. Covers all shapes in principle, but the trait bounds get awkward and the common case is harder to document than A.


If A sounds reasonable, I'll rework the PR along those lines. Happy to pivot if you'd prefer a different shape — e.g., keeping hyper-util/http-body-util out of the public surface, or something else I haven't considered.

@RReverser
Copy link
Copy Markdown
Owner

RReverser commented Apr 21, 2026

Quick note on why I don't think impl tower_service::Service alone gets us there: axum::Router::call takes http::Request<axum::body::Body> and produces http::Response<axum::body::Body>, so even behind impl Service<...> those associated types still carry axum::body::Body.

But Router's Service implementation is already generic over B.

If you just change the signature to be into_router<B>, that should be enough to return impl Service<Request<B>> not tied to axum at all, no?

@ivonnyssen
Copy link
Copy Markdown
Contributor Author

You're right — Router<()> is generic over B for the request, so fn into_router<B>(...) -> impl Service<http::Request<B>> works.

One bit to decide: the Response type pins to http::Response<axum::body::Body> (axum-core's alias), so the body type is reachable via <Svc as Service<_>>::Response even behind impl Service. Two versions:

// Simple — response body projects to axum::body::Body
pub fn into_router<B>(self) -> impl Service<http::Request<B>, Error = Infallible> + Clone + Send + 'static
where
    B: http_body::Body<Data = Bytes> + Send + 'static,
    B::Error: Into<BoxError>,
// Boxed response body — fully axum-free
pub fn into_router<B>(self) -> impl Service<
    http::Request<B>,
    Response = http::Response<UnsyncBoxBody<Bytes, BoxError>>,
    Error = Infallible,
> + Clone + Send + 'static
where
    B: http_body::Body<Data = Bytes> + Send + 'static,
    B::Error: Into<BoxError>,

The second is a one-liner .map(|b| b.map_err(Into::into).boxed_unsync()) inside. Which would you prefer?

@RReverser
Copy link
Copy Markdown
Owner

I don't really want to dig into details - feel free to investigate, I just want the API to remain as clean as possible and untied from axum :)

Add `Server::into_service()` returning a new `AlpacaService` newtype
that implements `tower_service::Service` for any `http::Request<B>`
with `B: Body<Data = Bytes>`. Responses use a type-erased
`UnsyncBoxBody<Bytes, BoxError>` so callers don't see axum types.

This supports TLS termination and custom middleware composition
without leaking axum as part of the public API surface.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
@ivonnyssen ivonnyssen force-pushed the pr/expose-into-router branch from 20da95f to c4d787c Compare April 22, 2026 19:43
ivonnyssen added a commit to ivonnyssen/ascom-alpaca-rs that referenced this pull request Apr 22, 2026
@ivonnyssen
Copy link
Copy Markdown
Contributor Author

I don't really want to dig into details - feel free to investigate, I just want the API to remain as clean as possible and untied from axum :)

Understood. I think the latest push should do that. No axum in the API and we have a way to support TLS and authentication.

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