From 23c303a0b5ab69d6232339411e404d69560af2ba Mon Sep 17 00:00:00 2001 From: Ioannis Igoumenos Date: Fri, 12 Jun 2026 10:56:08 +0300 Subject: [PATCH] Set Content-Type: application/json for API v2 responses --- app/config/routes.php | 5 + .../ForceJsonIfAcceptedMiddleware.php | 91 +++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 app/src/Middleware/ForceJsonIfAcceptedMiddleware.php diff --git a/app/config/routes.php b/app/config/routes.php index ca51b6952..9e505acae 100644 --- a/app/config/routes.php +++ b/app/config/routes.php @@ -22,6 +22,7 @@ */ use Cake\Http\Middleware\BodyParserMiddleware; +use App\Middleware\ForceJsonIfAcceptedMiddleware; use Cake\Http\Middleware\CsrfProtectionMiddleware; use Cake\Routing\Route\DashedRoute; use Cake\Routing\RouteBuilder; @@ -54,12 +55,16 @@ // BodyParserMiddleware will automatically parse JSON bodies, but we only // want that for API transactions, so we only apply it to the /api scope. $builder->registerMiddleware('bodyparser', new BodyParserMiddleware()); + $builder->registerMiddleware('forceJsonIfAccepted', new ForceJsonIfAcceptedMiddleware()); + /* * Apply a middleware to the current route scope. * Requires middleware to be registered through `Application::routes()` with `registerMiddleware()` */ $builder->setExtensions(['json']); $builder->applyMiddleware('bodyparser'); + $builder->applyMiddleware('forceJsonIfAccepted'); + // Use setPass to make parameter show up as function parameter // Model specific actions, which will usually have more specific URLs: // Note that while the UI uses dashes in URL paths, because we use {model} for the generic diff --git a/app/src/Middleware/ForceJsonIfAcceptedMiddleware.php b/app/src/Middleware/ForceJsonIfAcceptedMiddleware.php new file mode 100644 index 000000000..92e73d1a3 --- /dev/null +++ b/app/src/Middleware/ForceJsonIfAcceptedMiddleware.php @@ -0,0 +1,91 @@ +handle($request); + + if (!$this->clientWantsJson($request)) { + return $response; + } + + // If it's already JSON, leave it alone. + $contentType = strtolower($response->getHeaderLine('Content-Type')); + if ($contentType !== '' && str_contains($contentType, 'application/json')) { + return $response; + } + + // Otherwise override to JSON. + return $response->withHeader('Content-Type', 'application/json; charset=UTF-8'); + } + + /** + * Determine whether the client indicates it wants JSON. + * + * Currently supports an explicit route extension of "json" and Accept headers + * including "application/json" and vendor-specific "+json" media types. + * + * @since COmanage Registry v5.3.0 + * @param \Psr\Http\Message\ServerRequestInterface $request Request + * @return bool True if the client indicates JSON is acceptable/preferred + */ + private function clientWantsJson(ServerRequestInterface $request): bool + { + $params = $request->getAttribute('params'); + if (is_array($params) && (($params['_ext'] ?? null) === 'json')) { + return true; + } + + $accept = strtolower($request->getHeaderLine('Accept')); + + if (str_contains($accept, 'application/json')) { + return true; + } + + return preg_match('/application\/[^;,+]+\+json\b/', $accept) === 1; + } +}