MDL-86866 core: Web 404 responses should generate HTML not JSON

This commit is contained in:
Andrew Nicols
2026-02-19 12:17:22 +08:00
parent 2f9c784b34
commit 843611eb27
6 changed files with 95 additions and 30 deletions
+1 -2
View File
@@ -547,8 +547,7 @@ class component {
// Always keep moodle_exception in place.
$keyclasses = [
\core\exception\moodle_exception::class,
\core\output\bootstrap_renderer::class,
\core_cache\cache::class,
\core\router\middleware\api_validation_middleware::class,
];
foreach ($keyclasses as $classname) {
if (!array_key_exists($classname, $cache['classmap'])) {
+2 -1
View File
@@ -16,6 +16,7 @@
namespace core;
use core\router\middleware\api_validation_middleware;
use core\router\middleware\cors_middleware;
use core\router\middleware\error_handling_middleware;
use core\router\middleware\moodle_api_authentication_middleware;
@@ -251,7 +252,7 @@ class router {
// Add a Middleware to set the CORS headers for all REST Responses.
->add(di::get(cors_middleware::class))
->add(di::get(moodle_api_authentication_middleware::class))
->add(di::get(validation_middleware::class));
->add(di::get(api_validation_middleware::class));
}
/**
@@ -0,0 +1,49 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core\router\middleware;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
/**
* Middleware to handle validation of API requests and responses based on the route data.
*
* @package core
* @copyright Andrew Lyons <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class api_validation_middleware extends validation_middleware {
#[\Override]
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
try {
$request = $this->requestvalidator->validate_request($request);
} catch (\Exception $e) {
return $this->responsehandler->get_response_from_exception($request, $e);
}
$response = $handler->handle($request);
try {
$this->responsevalidator->validate_response($request, $response);
} catch (\Exception $e) {
return $this->responsehandler->get_response_from_exception($request, $e);
}
return $response;
}
}
@@ -53,27 +53,9 @@ class validation_middleware implements MiddlewareInterface {
#[\Override]
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface {
global $USER;
try {
$request = $this->requestvalidator->validate_request($request);
} catch (\Exception $e) {
$response = $this->responsehandler->get_response_from_exception($request, $e);
// Throw 'page not found' exception for non-admins.
// This hides stacktrace and errorcodes in detailed payload responses.
if (!is_siteadmin($USER->id) && $response->getStatusCode() == 404) {
return \core\router\util::throw_page_not_found($request, $response, $response->getReasonPhrase());
}
return $response;
}
$request = $this->requestvalidator->validate_request($request);
$response = $handler->handle($request);
try {
$this->responsevalidator->validate_response($request, $response);
} catch (\Exception $e) {
return $this->responsehandler->get_response_from_exception($request, $e);
}
$this->responsevalidator->validate_response($request, $response);
return $response;
}
+32 -1
View File
@@ -97,6 +97,37 @@ class util {
return self::redirect($response, $url);
}
/**
* Throw a specialized HTTP exception based on the response status code.
*
* @param \Psr\Http\Message\ServerRequestInterface $request
* @param \Psr\Http\Message\ResponseInterface $response
*/
public static function throw_specialized_exception(
ServerRequestInterface $request,
ResponseInterface $response,
?string $message = null,
?\Throwable $previous = null,
): void {
$exceptionclass = match ($response->getStatusCode()) {
400 => \Slim\Exception\HttpBadRequestException::class,
401 => \Slim\Exception\HttpUnauthorizedException::class,
403 => \Slim\Exception\HttpForbiddenException::class,
404 => \Slim\Exception\HttpNotFoundException::class,
405 => \Slim\Exception\HttpMethodNotAllowedException::class,
410 => \Slim\Exception\HttpGoneException::class,
429 => \Slim\Exception\HttpTooManyRequestsException::class,
500 => \Slim\Exception\HttpInternalServerErrorException::class,
501 => \Slim\Exception\HttpNotImplementedException::class,
default => null,
};
if ($exceptionclass !== null) {
throw new $exceptionclass($request, $message, $previous);
}
}
/**
* Generate a Page Not Found result.
*
@@ -109,7 +140,7 @@ class util {
ServerRequestInterface $request,
ResponseInterface $response,
): ResponseInterface {
throw new \Slim\Exception\HttpNotFoundException($request);
self::throw_specialized_exception($request, $response->withStatus(404));
}
/**
@@ -45,7 +45,7 @@ final class validation_middleware_test extends \advanced_testcase {
$requestvalidator->expects($this->once())
->method('validate_request')
->with($request)
->willThrowException(new \Exception('Invalid request'));
->willThrowException(new \Slim\Exception\HttpException($request, 'Invalid request'));
// If the request fails validation, it will not be passed to next Middleware.
$handler = $this->getMockBuilder(RequestHandlerInterface::class)->getMock();
@@ -60,8 +60,10 @@ final class validation_middleware_test extends \advanced_testcase {
// Execute the middleware.
$middleware = di::get(validation_middleware::class);
$returns = $middleware->process($request, $handler);
$this->assertInstanceOf(ResponseInterface::class, $returns);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Invalid request');
$middleware->process($request, $handler);
}
/**
@@ -97,9 +99,10 @@ final class validation_middleware_test extends \advanced_testcase {
// Execute the middleware.
$middleware = di::get(validation_middleware::class);
$returns = $middleware->process($request, $handler);
$this->assertInstanceOf(ResponseInterface::class, $returns);
$this->assertNotEquals($response, $returns);
$this->expectException(\Exception::class);
$this->expectExceptionMessage('Invalid response');
$middleware->process($request, $handler);
}
/**