Skip to content

Middleware Design

This document is based on the following source files:

Table of Contents

  1. Middleware Stack Overview
  2. Compression Middleware
  3. Access Log
  4. Panic Capture and Recovery
  5. Request ID
  6. CORS Configuration
  7. JWT Authentication Middleware
  8. Global HTTP Proxy
  9. SSRF Protection

1. Middleware Stack Overview

The Songloft backend is based on the Chi v5 routing framework. Middleware is registered in order within setupBaseRouter; requests pass through each middleware layer top to bottom, and responses return in reverse.

                        ┌─ Inbound request ─┐
                        │            │
                   ┌────▼────────────▼────┐
                   │   Compress (gzip)    │  ← Response compression
                   ├──────────────────────┤
                   │   AccessLog (slog)   │  ← Structured access log
                   ├──────────────────────┤
                   │ Tracely Panic Report │  ← Panic exception reporting
                   ├──────────────────────┤
                   │   Recoverer          │  ← Panic recovery, return 500
                   ├──────────────────────┤
                   │   RequestID          │  ← Inject unique request ID
                   ├──────────────────────┤
                   │   CORS               │  ← Cross-origin policy validation
                   └────────┬─────────────┘

              ┌─────────────┼─────────────┐
              │             │             │
         Public routes  Auth route group  Plugin route group
       (login/health)  (AuthMiddleware) (AuthMiddleware
                                       + PublicPathChecker)

Global middleware applies to all requests; AuthMiddleware is injected only into route groups that require authentication, via r.Group + r.Use.

Section sources


2. Compression Middleware

Compression is split into two layers: Chi's built-in gzip middleware handles dynamic responses, and precompressedFS handles static assets that were precompressed at build time.

2.1 Chi gzip Middleware

Registered via chi_middleware.Compress(5, ...), with compression level 5 (a balance of speed and compression ratio), and only applies to the following MIME types:

text/html, text/css, text/plain, text/javascript, application/javascript, application/json, application/wasm, image/svg+xml, font/otf

Audio files (audio/*) and already-compressed images (image/png, etc.) are not in the list, avoiding ineffective compression.

2.2 Precompressed Static Assets (precompressedFS)

If the brotli CLI is installed at build time, the Makefile generates .br and .gz precompressed files for the frontend static assets. newPrecompressedFS loads these files into memory from embed.FS at startup.

Request handling logic:

  1. First check whether Accept-Encoding contains br; on a hit, return the brotli-compressed version
  2. Next check gzip; on a hit, return the gzip version
  3. When neither is supported, read the raw file from embed.FS
  4. When the precompressed cache misses, fall back to http.FileServer + Chi gzip middleware

ETags are generated from a CRC32 checksum and support If-None-Match conditional requests returning 304. addCustomEntry is used for files modified at runtime (such as index.html after base-path injection) to recompress and replace the cache.

Section sources


3. Access Log

The access log is bridged to the standard library slog via slogLogFormatter, replacing Chi's default chi_middleware.Logger (which writes directly to the log package and is not controlled by the runtime log level).

Log Fields

FieldDescription
methodHTTP method (GET/POST, etc.)
pathRequest path
statusResponse status code
bytesResponse body byte count
dur_msRequest duration (milliseconds, microsecond precision)
remoteClient remote address
request_idThe unique ID injected by the Chi RequestID middleware (optional)

The log level is Info; when an administrator raises the runtime level to Warn or Error via /settings/log-level, the access log automatically goes silent. Panic events are logged at the Error level with a full call stack.

Section sources


4. Panic Capture and Recovery

Panic handling is split into two layers, and the registration order is critical:

  1. Tracely reporting (outer) -- captures the panic in a defer recover, reports an ErrorPayload (containing type, message, stack, URL) to the Tracely error-tracking service, and then re-panics
  2. Chi Recoverer (inner) -- captures the re-thrown panic, returns a 500 response to the client, and prevents the process from crashing

This design ensures the exception is both reported to the monitoring system and not leaked to the client.

Section sources


5. Request ID

chi_middleware.RequestID injects a unique identifier for each request into the request context. The access log middleware extracts this ID from the context and writes it to the request_id field, used for distributed tracing and log correlation.


6. CORS Configuration

The CORS middleware uses the go-chi/cors package, customizing origin validation via AllowOriginFunc:

Origin ruleMatch scope
http://localhost:*Local development (any port)
http://127.0.0.1:*Local loopback (any port)
http://192.168.* / http://10.* / http://172.16.*LAN ranges (HTTP only)
http(s)://hanxi.cc(:port)Project main domain (HTTP/HTTPS, any port)
http(s)://*.hanxi.cc(:port)All subdomains (HTTP/HTTPS, any port)

Other configuration options:

  • AllowedMethods: GET, HEAD, POST, PUT, DELETE, OPTIONS
  • AllowedHeaders: Accept, Authorization, Content-Type
  • AllowCredentials: true (allows carrying Cookie/Authorization)
  • MaxAge: 300 seconds (preflight request cached for 5 minutes)

Section sources


7. JWT Authentication Middleware

AuthMiddleware is a route-group-level authentication guard that protects all API endpoints requiring login.

7.1 Token Extraction Strategy

Extracts the JWT from two locations, in priority order:

  1. Authorization Header -- Bearer <token> format, the standard HTTP authentication method
  2. Query Parameter fallback -- ?access_token=<token>, for scenarios where a custom request header cannot be set (<img> tags, CachedNetworkImage, audio stream URLs, etc.)

7.2 XiaoAi Quirk

The XiaoAi speaker firmware has a known defect: it replaces & in the URL with a space, causing query parameters after access_token to be merged into the token value. The middleware handles this compatibility:

  1. Detect whether the access_token value contains a space (a valid JWT contains no spaces)
  2. Split by space, taking the first segment as the real token
  3. Parse subsequent segments in key=value format and restore them into r.URL.Query()
  4. Re-encode r.URL.RawQuery, ensuring downstream handlers receive the correct query parameters

7.3 Public Path Bypass (PublicPathChecker)

PublicPathChecker is an interface; implementers declare which paths require no authentication via IsPublicPath(path) bool. At the middleware entry, all registered checkers examine the request path in turn; if any returns true, JWT validation is skipped and the request passes through.

Current use case: the JS plugin manager implements this interface, and the publicPaths declared in a plugin manifest (such as Subsonic /rest/* compatibility endpoints) are registered at runtime as public paths, with hot-update support requiring no restart.

7.4 Authentication Failure Response

Returns the unified JSON format {"error": "<message>", "detail": "<err>"} together with an HTTP 401 status code, conforming to the project's API response conventions.

Section sources


8. Global HTTP Proxy

internal/httputil/proxy.go provides process-level HTTP proxy configuration; all clients created via httputil.NewClient automatically use the proxy.

Core Components

ComponentDescription
proxyConfigThread-safe holder of the proxy URL, protected for read/write by sync.RWMutex
sharedTransportThe globally shared *http.Transport, with connection pool config: max idle connections 100, max 10 per host, idle timeout 90 seconds
SetGlobalProxy(rawURL)Set the proxy address (supports HTTP/HTTPS/SOCKS5); an empty string clears the proxy; after the call, CloseIdleConnections cleans up old connections
GetGlobalProxy()Get the current proxy address
NewClient(timeout)Create an *http.Client that uses the global proxy

Loopback Auto-Bypass

proxyFunc checks the target hostname on each request: the three loopback addresses localhost, 127.0.0.1, and ::1 directly return nil (no proxy), preventing internal requests from being intercepted by the proxy.

URL Basic Auth Extraction

httputil.ApplyBasicAuthFromURL is a helper function: it extracts the username and password from req.URL.User, sets them into the Authorization request header, and then clears req.URL.User, preventing credentials from leaking into logs. Used for scenarios where authentication info is embedded in the proxy URL.

Section sources


9. SSRF Protection

services.IsHostnameAllowed adopts an intranet-blocking policy: it blocks access to all intranet addresses to prevent SSRF attacks, while allowing all external domains. This function is used in scenarios such as the HLS proxy where the server needs to initiate external requests.

Check Flow

  1. Hostname blacklist -- localhost, *.local, and empty strings are rejected directly
  2. DNS resolution -- run net.LookupIP on the domain; on resolution failure, let it through (leaving the subsequent HTTP request to report the error itself)
  3. IP address classification check -- examine each resolved IP one by one:
CheckCoverage
IsLoopback127.0.0.0/8, ::1
IsPrivate10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7
IsLinkLocalUnicast/Multicast169.254.0.0/16, fe80::/10
IsUnspecified0.0.0.0, ::

If any IP falls within an intranet/reserved address range it is rejected; only when all pass is the request allowed.

Section sources