Contents

diyamantina/clientipmiddleware

Vapor middleware for carrying client identity into OpenAPI-generated handlers.

Installation

.package(url: "https://github.com/diyamantina/ClientIpMiddleware", from: "1.0.0"),
.target(
    name: "ApiServer",
    dependencies: [
        .product(name: "ClientIpMiddleware", package: "ClientIpMiddleware"),
    ]
),

Quick Start

Install the middleware once during Vapor boot:

import ClientIpMiddleware
import Vapor

let app = try await Application.make(.detect())

// Direct deployment, local development, or no trusted proxy in front.
app.middleware.use(ClientIpMiddleware())

// Behind one trusted reverse proxy, such as nginx, Caddy, ALB, or Cloudflare.
app.middleware.use(ClientIpMiddleware(trustedProxyHops: 1))

// Behind two trusted hops, such as CDN -> reverse proxy -> app.
app.middleware.use(ClientIpMiddleware(trustedProxyHops: 2))

Read the values from any async handler executed during the request:

import ClientIpMiddleware

func someOperation(...) async throws -> ... {
    let ip = ClientContext.clientIp ?? "unknown"
    let userAgent = ClientContext.userAgent ?? "unknown"

    // Log, audit, rate-limit, or pass the values to your domain layer.
}

The same access pattern works in generated OpenAPI handlers and conventional Vapor route handlers.

Client IP Resolution

trustedProxyHops controls whether X-Forwarded-For is trusted.

| Setting | Source | |---|---| | 0 | Ignore X-Forwarded-For; use the connection remoteAddress?.ipAddress. This is the secure default for direct deployments. | | 1 | Trust one proxy in front of the app and use the rightmost usable X-Forwarded-For entry. | | n >= 2 | Trust n proxy hops and select the entry at count - n, falling back to remoteAddress when the header is absent or too short. |

Match trustedProxyHops to your actual deployment. Setting it too high lets a client spoof an address by adding extra X-Forwarded-For entries; setting it too low reports a proxy address instead of the client.

Task-Local Context

Captured values are exposed through ClientContext:

public enum ClientContext {
    @TaskLocal public static var clientIp: String?
    @TaskLocal public static var userAgent: String?
}

The middleware sets these inside nested withValue scopes around the downstream responder. Values are visible through async calls made during the request and are cleared when the request leaves the middleware chain.

Companion Packages

stamps bearer tokens on generated OpenAPI clients and enforces bearer tokens in Vapor servers.

logs OpenAPI requests and responses on both client and server paths, with default credential-header redaction.

Platform Support

| Platform | CI coverage | |---|---| | macOS 13+ | swift build and swift test | | Linux, Swift 6.0 container | swift build and swift test | | iOS / tvOS / watchOS | Not supported because Vapor does not ship for these platforms |

Documentation

DocC is enabled:

swift package --allow-writing-to-directory ./docs generate-documentation \
    --target ClientIpMiddleware \
    --output-path ./docs/ClientIpMiddleware.doccarchive

Testing

The package ships with 33 tests across 3 suites covering:

  • ClientContext task-local semantics.
  • Vapor middleware integration.
  • trustedProxyHops behavior for direct, single-proxy, and multi-proxy setups.
  • Spoof resistance for malformed or adversarial X-Forwarded-For values.
  • Cross-request isolation and propagation across await.

Run them with:

swift test

Contributing

Contributions are welcome. Before opening a PR, read CONTRIBUTING.md, CODE_OF_CONDUCT.md, SECURITY.md, and SUPPORT.md.

Notable changes are recorded in CHANGELOG.md.

License

Apache 2.0. See LICENSE.

Package Metadata

Repository: diyamantina/clientipmiddleware

Default branch: main

README: README.md