Skip to content

Unify plain and front-end startup into a single handler pipeline #6

@hugithordarson

Description

@hugithordarson

Context

Iteration 1 currently has two parallel startup paths in Modulo:

  • startPlain() — a single plain-HTTP connector on port 1400 going straight to ModuloProxy. The pre-iteration-1 behavior, kept as a safety net during the front-end's first deployments.
  • startWithFrontend() — a fresh Jetty Server with TCP TLS + (optionally) QUIC connectors, wrapped in the full handler chain (ACME challenge → HTTPS redirect → canonical-host redirect → compression → proxy).

These two paths share no structure. They build two separate Server instances, with two separate handler trees, and there's no notion of "the chain ran" for traffic that arrived on port 1400. That made sense in iteration 1 — we needed today's behavior preserved bit-for-bit while introducing the new path — but it's not the right end state.

The insight

There's exactly one piece of logic that fundamentally matters: "decide where this request goes." Everything else (ACME challenge passthrough, HTTPS redirect, canonical-host redirect, compression, etc.) is additive policy sitting in front of that one thing.

Each of those additive handlers is either:

  • Inert for traffic that doesn't need it (ACME challenge handler only fires on /.well-known/acme-challenge/*; compression skips when client doesn't support it).
  • Safe to apply behind a trusted upstream when the upstream signals via X-Forwarded-Proto / Forwarded that the right thing already happened (HTTPS redirect, canonical-host redirect).

So the architectural endgame is one handler pipeline fed by multiple connectors, with per-connector trust hints that tell the pipeline whether to trust X-Forwarded-* (legacy-plain connector behind Apache) or its own observation (own-the-front-door TLS connector). The "frontend vs plain proxy" distinction collapses into "which connectors are configured, and what trust profile each one carries."

Target shape

Modulo
├── connectors (config-driven, any combination)
│   ├── plain HTTP on 1400   (legacy upstream-trust connector)
│   ├── TLS on 443           (own-the-front-door connector)
│   ├── QUIC on 443          (HTTP/3, same trust profile as TLS)
│   └── ...
└── handler chain (single, not duplicated)
    ├── ACME challenge passthrough
    ├── HTTPS / canonical-host / etc. policy handlers (consult connector trust)
    ├── compression
    └── proxy (the one thing modulo fundamentally is)

This also lines up with the "modulo as a library" direction from undur/wo-adaptor-jetty#2: the reusable thing is the chain. A WO/ng app embedding the chain swaps the terminal proxy for its own handler. Same connectors, same policy handlers, different terminus.

Concrete refactor sketch

  1. Collapse startPlain and startWithFrontend into one builder that:
    • Builds the proxy.
    • Builds the (single) handler chain wrapping it.
    • Adds each configured connector to one Server.
  2. Each connector carries a small trust descriptor: own-the-front-door vs behind-trusted-upstream. The policy handlers consult it to decide whether request.isSecure() should come from the connector or from X-Forwarded-Proto.
  3. The legacy plain connector becomes a config option (off by default once front-end is the norm, on for transitional deployments).

Net effect should be substantially less code than today — the duplication that exists in iteration 1 disappears.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions