Skip to content

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.

app/Http/Kernel.php
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,
];

File: app/Http/Middleware/InjectTraceId.php

Injects OpenTelemetry trace ID into Sentry and Datadog for APM correlation.

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;
}
  • Excludes URIs matching config('apm.exclude_uri_prefixes')
  • Generates Datadog trace logs URL in Sentry context
  • Adds datadog.trace_id tag for Sentry searchability
Trace-Id: abc123def456...

File: app/Http/Middleware/RemoveUselessVary.php

Removes the Vary header from non-cacheable responses.

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 Vary header
  • Response is not cacheable (based on Cache-Control directives)

File: app/Http/Middleware/SetCalcEngineVersion.php

Allows switching calculation engine versions in non-production environments for testing.

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));
}
  • Disabled in production and unit tests
  • Reads version from header X-RightCapital-EngineVersion or cookie engine_version
  • Uses Context::setEngineVersion() to store version
  • Appends X-RightCapital-EngineVersion to CORS Access-Control-Allow-Headers
Terminal window
# Via header
curl -H "X-RightCapital-EngineVersion: v2" https://staging.example.com/api/calculate
# Via cookie
curl --cookie "engine_version=v2" https://staging.example.com/api/calculate

File: Laravel’s built-in Illuminate\Http\Middleware\HandleCors

Standard Laravel CORS handling with configuration in config/cors.php.

config/cors.php
return [
'paths' => ['*'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];

File: app/Http/Middleware/TrustProxies.php

Configures trusted proxy settings for AWS ELB and other reverse proxies.

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;
}

Ensures the application correctly identifies:

  • Client IP address ($request->ip())
  • Request scheme (HTTP/HTTPS)
  • Request host and port

File: app/Http/Middleware/PreventRequestsDuringMaintenance.php

Extended Laravel maintenance mode with mobile app bypass and custom messages.

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
}
}
  • 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="..."

File: Laravel’s built-in Illuminate\Foundation\Http\Middleware\ValidatePostSize

Validates that POST request body doesn’t exceed PHP’s post_max_size limit.

{
"message": "The POST content length exceeded the allowed limit."
}

HTTP Status: 413 Payload Too Large


File: app/Http/Middleware/TrimStrings.php

Automatically trims whitespace from all string inputs.

final class TrimStrings extends Middleware
{
protected $except = [
'password',
'password_confirmation',
'disclosure',
];
}
  • Input: " hello world "
  • Output: "hello world"
  • Except: password, password_confirmation, disclosure fields are NOT trimmed

File: packages/libs/laravel-http-cache-control/

Adds HTTP cache control headers including ETags and supports 304 Not Modified responses.

  • Adds Cache-Control header based on response type
  • Computes weak ETags for text responses
  • Returns 304 Not Modified for matching ETags
  • Supports public/private cache directives
config/http-cache.php
return [
'etag' => true,
'default_ttl' => 0,
'private' => true,
];

See [packages.md](../packages/ for detailed implementation.


File: app/Http/Middleware/CheckAcceptHeader.php

Enforces that API requests include a proper Accept header.

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);
}

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


File: app/Http/Middleware/CheckUserAgentHeader.php

Requires a User-Agent header on all requests for client identification.

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);
}
{
"message": "User-Agent header is required in your request."
}

HTTP Status: 400 Bad Request


File: app/Http/Middleware/CheckMobileAppVersion.php

Validates mobile app version for API compatibility.

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);
}
  • Uses MobileAppUserAgentParser to detect mobile apps
  • Uses Composer’s Semver::satisfies() for version comparison
  • Returns RFC 7807 problem response format
  • Required: >= 2.12.0
{
"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)


MiddlewarePurposeError Code
InjectTraceIdAPM tracing-
RemoveUselessVaryCache optimization-
SetCalcEngineVersionEngine switching (non-prod)-
HandleCorsCORS headers-
TrustProxiesProxy trust-
PreventRequestsDuringMaintenanceMaintenance mode503
ValidatePostSizeRequest size limit413
TrimStringsInput sanitization-
SetCacheHeadersHTTP caching-
CheckAcceptHeaderContent negotiation406
CheckUserAgentHeaderClient identification400
CheckMobileAppVersionApp version check400