Global Middleware
Global middleware is applied to every HTTP request before it reaches route-specific middleware. These are registered in app/Http/Kernel.php under the $middleware array.
Middleware Execution Order
Section titled “Middleware Execution Order”protected $middleware = [ InjectTraceId::class, RemoveUselessVary::class, SetCalcEngineVersion::class, HandleCors::class, TrustProxies::class, PreventRequestsDuringMaintenance::class, ValidatePostSize::class, TrimStrings::class, SetCacheHeaders::class, CheckAcceptHeader::class, CheckUserAgentHeader::class, CheckMobileAppVersion::class,];1. InjectTraceId
Section titled “1. InjectTraceId”File: app/Http/Middleware/InjectTraceId.php
Injects OpenTelemetry trace ID into Sentry and Datadog for APM correlation.
Implementation
Section titled “Implementation”public function handle(Request $request, Closure $next): SymfonyResponse{ $need_to_inject = array_filter( config('apm.exclude_uri_prefixes'), function (string $prefix) use ($request): bool { return str_starts_with($request->getRequestUri(), $prefix); }, ) === [];
if ($need_to_inject === false) { return $next($request); }
$trace_id = Span::fromContext(Context::getCurrent())->getContext()->getTraceId();
configureScope(function (Scope $scope) use ($trace_id): void { $reduced_precision_timestamp_ms = substr_replace(strval(time()), '00', 8, 2) . '000'; $plus_two_minute_timestamp_ms = intval($reduced_precision_timestamp_ms) + 120_000;
$query = http_build_query([ 'query' => 'trace_id:' . $trace_id, 'from_ts' => $reduced_precision_timestamp_ms, 'to_ts' => $plus_two_minute_timestamp_ms, 'live' => 'false', ], encoding_type: PHP_QUERY_RFC3986);
$scope->setContext('Datadog Trace Details', [ 'Trace ID' => $trace_id, 'Trace Logs' => 'https://app.datadoghq.com/logs?' . $query, ]);
$scope->setTag('datadog.trace_id', $trace_id); });
$response = $next($request);
if ($response instanceof SymfonyResponse) { $response->headers->set(self::HEADER_TRACE_ID, $trace_id); }
return $response;}Key Features
Section titled “Key Features”- Excludes URIs matching
config('apm.exclude_uri_prefixes') - Generates Datadog trace logs URL in Sentry context
- Adds
datadog.trace_idtag for Sentry searchability
Response Header
Section titled “Response Header”Trace-Id: abc123def456...2. RemoveUselessVary
Section titled “2. RemoveUselessVary”File: app/Http/Middleware/RemoveUselessVary.php
Removes the Vary header from non-cacheable responses.
Implementation
Section titled “Implementation”public function handle(Request $request, Closure $next): Response{ $response = $next($request);
if ($response->hasVary() && !$response->isCacheable()) { $response->headers->remove('Vary'); }
return $response;}Only removes Vary when both conditions are true:
- Response has a
Varyheader - Response is not cacheable (based on
Cache-Controldirectives)
3. SetCalcEngineVersion
Section titled “3. SetCalcEngineVersion”File: app/Http/Middleware/SetCalcEngineVersion.php
Allows switching calculation engine versions in non-production environments for testing.
Implementation
Section titled “Implementation”public function handle(Request $request, Closure $next): Response{ if (App::isProduction() || App::runningUnitTests()) { return $next($request); }
if ($this->isPreflightRequest($request)) { return $this->appendEngineVersionToAllowHeaders($next($request)); }
$engine_version = $request->header('X-RightCapital-EngineVersion') ?? $request->cookie('engine_version');
if ($engine_version !== null) { Context::setEngineVersion($engine_version); }
return $this->appendEngineVersionToAllowHeaders($next($request));}Key Features
Section titled “Key Features”- Disabled in production and unit tests
- Reads version from header
X-RightCapital-EngineVersionor cookieengine_version - Uses
Context::setEngineVersion()to store version - Appends
X-RightCapital-EngineVersionto CORSAccess-Control-Allow-Headers
# Via headercurl -H "X-RightCapital-EngineVersion: v2" https://staging.example.com/api/calculate
# Via cookiecurl --cookie "engine_version=v2" https://staging.example.com/api/calculate4. HandleCors
Section titled “4. HandleCors”File: Laravel’s built-in Illuminate\Http\Middleware\HandleCors
Standard Laravel CORS handling with configuration in config/cors.php.
Configuration
Section titled “Configuration”return [ 'paths' => ['*'], 'allowed_methods' => ['*'], 'allowed_origins' => ['*'], 'allowed_origins_patterns' => [], 'allowed_headers' => ['*'], 'exposed_headers' => [], 'max_age' => 0, 'supports_credentials' => true,];5. TrustProxies
Section titled “5. TrustProxies”File: app/Http/Middleware/TrustProxies.php
Configures trusted proxy settings for AWS ELB and other reverse proxies.
Implementation
Section titled “Implementation”final class TrustProxies extends Middleware{ protected $proxies = '*';
protected $headers = Request::HEADER_X_FORWARDED_FOR |Request::HEADER_X_FORWARDED_HOST |Request::HEADER_X_FORWARDED_PORT |Request::HEADER_X_FORWARDED_PROTO |Request::HEADER_X_FORWARDED_AWS_ELB;}Purpose
Section titled “Purpose”Ensures the application correctly identifies:
- Client IP address (
$request->ip()) - Request scheme (HTTP/HTTPS)
- Request host and port
6. PreventRequestsDuringMaintenance
Section titled “6. PreventRequestsDuringMaintenance”File: app/Http/Middleware/PreventRequestsDuringMaintenance.php
Extended Laravel maintenance mode with mobile app bypass and custom messages.
Implementation
Section titled “Implementation”class PreventRequestsDuringMaintenance extends Middleware{ private const string MAINTENANCE_MODE_HEADER_NAME = 'x-rightcapital-maintenance-mode';
protected $except = [ '/statuses', '/statuses/*', '/v2/debug/reset-environment', '/v2/webhooks/*', ];
public function handle($request, Closure $next): Response { // CORS preflight bypass if ($request->method() === Request::METHOD_OPTIONS) { return $next($request); }
if ($this->app->isDownForMaintenance()) { $maintenance_mode_data = $this->app->maintenanceMode()->data();
// Mobile app bypass with secret validation if (new MobileAppUserAgentParser($request->userAgent())->isMobileApp() && isset($maintenance_mode_data['secret']) && $request->header(self::MAINTENANCE_MODE_HEADER_NAME) === $maintenance_mode_data['secret']) { return $next($request); } }
// ... parent handling with custom message support }}Key Features
Section titled “Key Features”- Bypasses OPTIONS (CORS preflight) requests
- Excludes specific URIs (
/statuses, webhooks, etc.) - Mobile app bypass requires matching secret in header
- Supports custom maintenance message from
php artisan down --message="..."
7. ValidatePostSize
Section titled “7. ValidatePostSize”File: Laravel’s built-in Illuminate\Foundation\Http\Middleware\ValidatePostSize
Validates that POST request body doesn’t exceed PHP’s post_max_size limit.
Error Response
Section titled “Error Response”{ "message": "The POST content length exceeded the allowed limit."}HTTP Status: 413 Payload Too Large
8. TrimStrings
Section titled “8. TrimStrings”File: app/Http/Middleware/TrimStrings.php
Automatically trims whitespace from all string inputs.
Implementation
Section titled “Implementation”final class TrimStrings extends Middleware{ protected $except = [ 'password', 'password_confirmation', 'disclosure', ];}Behavior
Section titled “Behavior”- Input:
" hello world " - Output:
"hello world" - Except:
password,password_confirmation,disclosurefields are NOT trimmed
9. SetCacheHeaders
Section titled “9. SetCacheHeaders”File: packages/libs/laravel-http-cache-control/
Adds HTTP cache control headers including ETags and supports 304 Not Modified responses.
Features
Section titled “Features”- Adds
Cache-Controlheader based on response type - Computes weak ETags for text responses
- Returns
304 Not Modifiedfor matching ETags - Supports public/private cache directives
Configuration
Section titled “Configuration”return [ 'etag' => true, 'default_ttl' => 0, 'private' => true,];See [packages.md](../packages/ for detailed implementation.
10. CheckAcceptHeader
Section titled “10. CheckAcceptHeader”File: app/Http/Middleware/CheckAcceptHeader.php
Enforces that API requests include a proper Accept header.
Implementation
Section titled “Implementation”public function handle(Request $request, Closure $next): SymfonyResponse{ if (!$request->acceptsJson()) { return response( 'Our APIs can only communicate in JSON. This requires the ACCEPT header in your request to be application/json or */*.', Response::HTTP_NOT_ACCEPTABLE, ['Content-Type' => 'text/plain; charset=utf-8'] ); }
return $next($request);}Error Response
Section titled “Error Response”Plain text response (NOT JSON):
Our APIs can only communicate in JSON. This requires the ACCEPT header in your request to be application/json or */*.HTTP Status: 406 Not Acceptable
Content-Type: text/plain; charset=utf-8
11. CheckUserAgentHeader
Section titled “11. CheckUserAgentHeader”File: app/Http/Middleware/CheckUserAgentHeader.php
Requires a User-Agent header on all requests for client identification.
Implementation
Section titled “Implementation”public function handle(Request $request, Closure $next): Response{ if ($request->headers->get('User-Agent', '') === '') { throw new BadRequestHttpException('User-Agent header is required in your request.'); }
return $next($request);}Error Response
Section titled “Error Response”{ "message": "User-Agent header is required in your request."}HTTP Status: 400 Bad Request
12. CheckMobileAppVersion
Section titled “12. CheckMobileAppVersion”File: app/Http/Middleware/CheckMobileAppVersion.php
Validates mobile app version for API compatibility.
Implementation
Section titled “Implementation”private const string ACCEPTABLE_VERSION_RANGE = '>=2.12.0';
public function handle(Request $request, Closure $next): SymfonyResponse{ $user_agent_parser = new MobileAppUserAgentParser($request->userAgent());
if ($user_agent_parser->isMobileApp()) { $app_version = $user_agent_parser->getAppVersion();
if ($app_version === null || !str_contains($app_version, '.') || !Semver::satisfies($app_version, self::ACCEPTABLE_VERSION_RANGE)) { return problem_response( 'Your RightCapital app version is no longer supported, please update to the latest version.', Response::HTTP_BAD_REQUEST, ['code' => PROBLEM_CODE_MOBILE_APP_VERSION_NOT_SUPPORTED], null, 'mobile_app/outdated', null ); } }
return $next($request);}Key Features
Section titled “Key Features”- Uses
MobileAppUserAgentParserto detect mobile apps - Uses Composer’s
Semver::satisfies()for version comparison - Returns RFC 7807 problem response format
Minimum Version
Section titled “Minimum Version”- Required:
>= 2.12.0
Error Response
Section titled “Error Response”{ "type": "mobile_app/outdated", "title": "Your RightCapital app version is no longer supported, please update to the latest version.", "status": 400, "code": "MOBILE_APP_VERSION_NOT_SUPPORTED"}HTTP Status: 400 Bad Request (NOT 426)
Summary
Section titled “Summary”| Middleware | Purpose | Error Code |
|---|---|---|
InjectTraceId | APM tracing | - |
RemoveUselessVary | Cache optimization | - |
SetCalcEngineVersion | Engine switching (non-prod) | - |
HandleCors | CORS headers | - |
TrustProxies | Proxy trust | - |
PreventRequestsDuringMaintenance | Maintenance mode | 503 |
ValidatePostSize | Request size limit | 413 |
TrimStrings | Input sanitization | - |
SetCacheHeaders | HTTP caching | - |
CheckAcceptHeader | Content negotiation | 406 |
CheckUserAgentHeader | Client identification | 400 |
CheckMobileAppVersion | App version check | 400 |