This is an RFC for the Handler interface of the proposed new REST API for MediaWiki.
The purpose of this RFC is to establish a bare-bones interface for core and extension REST API handlers. We want to achieve consensus on the elements proposed, to the extent necessary to avoid backwards-incompatible changes in the foreseeable future.
This RFC is not intended to be feature-complete and is not a general discussion of the REST API. There will presumably be other RFCs on other elements of the REST API prior to its public deployment.
There is a work-in-progress patch showing the proposed details at Gerrit 508972.
Glossary
- “Route” - a method name and URL pattern for a group of related URLs, with named parameters.
- “Handler” - a code unit (function or class or object) responsible for accepting HTTP requests to a given route and
- “Router” - accepts HTTP requests for the REST API, determines which route the request maps best to, and executes the handler, returning the results as an HTTP response.
Route declaration
There would be an extension attribute called RestRoutes, which is an array of routes, for example:
"RestRoutes": [ { "method": "GET", "path": "/user/{userName}/hello", "class": "HelloHandler", "parameters": [ { "name": "userName", "in": "path", "required": true, "type": "string" } ] } ]
The keys in this JSON route object are:
- method: A string HTTP method name. Or an array of method names, which is equivalent to declaring multiple routes, identical except for their method.
- path: A path template. Parameter names are enclosed in braces. A parameter must be a whole path component, where path components are separated by slashes.
- class, factory, args, calls: Parameters passed to ObjectFactory::getObjectFromSpec() to create the Handler object. Further considerations and future directions for object construction and dependency injection are discussed at T222409.
- parameters: The syntax in this example is derived from OpenAPI.
Routes in core would be declared in a JSON file, say includes/Rest/coreRoutes.json.
RequestInterface
The proposed RequestInterface is identical to PSR-7's ServerRequestInterface, except:
- The cloning mutation methods "with..." are omitted. These could be added at a later date, when the need arises.
- Mutation of attributes is supported via setAttributes()
- Prefixed cookie names are supported with getCookie()
- Instead of getParsedBody(), we provide getPostParams(), which is strictly a $_POST wrapper, it is not used for other request body deserialization. This modification came out of a Gerrit discussion of the drawbacks of getParsedBody(). In our opinion, getParsedBody() represented an inappropriate mixing of $_POST with extensible validation and deserialization of other types of request body.
- Instead of getAttributes(), we have getPathParams().
The WIP patch proposes two implementations of this interface: RequestFromGlobals and RequestData. RequestFromGlobals gets its data on demand directly from the global state, for example the superglobals. RequestData gets its data from an associative array passed to its constructor, with sensible defaults for omitted parameters. RequestData is intended for internal requests and testing.
Why can't we inject global state into RequestData for use in an externally triggered request? There are three details that make this inconvenient:
- RequestInterface has getParsedBody(), which returns structured data representing the body of the POST request. In RequestFromGlobals the parsing is done on demand via an injected BodyParserFactory. In RequestData the parsed body is injected into the constructor.
- RequestInterface has getBody() which returns a stream representing the request body. In RequestFromGlobals, this stream is automatically derived from php://input. In RequestData, the request body, if any, is a string, converted to a stream by the constructor.
- RequestInterface has getUploadedFiles(), which returns an array of UploadedFileInterface objects. In RequestFromGlobals this array is lazy-initialized from $_FILES. In Request, the objects are constructed by the caller.
In summary, RequestData and RequestFromGlobals have different constructor parameters, responding to the different needs of internal versus external callers.
I'm proposing that RequestFromGlobals gets its data from the superglobals directly, instead of from $wgRequest. My feeling is that WebRequest, with its direct access to the superglobals in the base class, is awkward for testability, and could be improved by having it take a Rest\RequestInterface in its constructor. The superglobals in WebRequest would be replaced by injected data from a RequestInterface.
ResponseInterface
The proposed ResponseInterface is similar to PSR-7's ResponseInterface, except:
- For convenience and efficiency, the cloning mutator methods "with..." have been replaced by non-cloning mutator methods "set...". We considered PSR-7's rationale for making ResponseInterface immutable, but did not accept it.
- getRawHeaderLines() was added, for convenient access to header lines.
- setCookie() was added, for cookie prefixing and serialization.
The response contains the body text as a Psr\Http\Message\StreamInterface object. This may be a simple wrapper around a string. Guzzle (already a core dependency) provides several convenient classes derived from StreamInterface. Using Guzzle, a stream can be constructed which reads from a local file or HTTP client request.
Handler
Handler is the abstract base class for route handlers.
Handler itself requires no constructor parameters. Constructor parameters may be used by subclasses to identify a particular route to a shared handler.
State is injected into the handler object by the Router shortly after its construction, using an init() method. Handler provides accessors such as:
- getRequest(): The RequestInterface object.
- getConfig(): The configuration array of the route, decoded from the JSON.
- getResponseFactory(): A ResponseFactory object, containing a variety of convenient factory functions for creation of a response object.
- getValidator(): A helper object for parameter validation.
Router::executeHandler() is equivalent to ApiBase::executeAction(), and performs the following tasks:
- Parameter validation
- Authorization
- Conditional 304 responses
- Calling of the abstract execute() function
- Optionally, construction of the Response object from a scalar or array return value.
The following methods will have default implementations, but may also be overridden by the subclass:
- getParameters(): Allowing the parameter declaration to be dynamically constructed, instead of derived from JSON
- getLastModified(), getETag(): The last-modified time and ETag for 304 handling, similar to ApiBase::getConditionalRequestData()
- There will presumably also be documentation/reflection methods, for example an equivalent to ApiBase::getSummaryMessage()
SimpleHandler
SimpleHandler maps path template parameters to formal parameters of the run() method, providing slightly simpler code:
class SimpleHandler extends Handler { public function execute() { $params = array_values( $this->getRequest()->getAttributes() ); return $this->run( ...$params ); } } class HelloHandler extends SimpleHandler { public function run( $name ) { return "Hello, $name!"; } }
Using a factory function, handler classes may be anonymous.
Router
class Router { public function execute( RequestInterface $request ): ResponseInterface { ... } }
Router::execute() takes a request object, matches the path against the list of known path templates, and constructs a handler. It sets the request attributes to be the path template parameters derived from the template match. It executes the handler, and returns the response object.
Router catches exceptions from the Handler. If the exception is derived from RestException, the exception is not logged, the exception is just converted to a normal response. Other exceptions will be dealt with in a similar way to the Action API.
It is proposed to make Router be a service. This is controversial because it implies that internal requests will be encouraged.
EntryPoint
There will be a new entry point file, rest.php, which invokes the static method MediaWiki\Rest\EntryPoint::main(). This method constructs a request object, executes the router, and outputs the response.
The root path for routing is defined by $wgRestPath.
Open questions
- What should standard responses look like? For example, standard 404s and parameter validation errors. Should they be JSON? Localised?
- ResponseFactory methods and their arguments need to be defined. What sorts of responses will endpoints typically need?
- Is ResponseFactory responsible for localisation?
- Do iteration/index routes require any special handling?
- The current path template matcher allows only string literals and wildcards. Conflicting paths are not allowed. Is this sufficient? This means that for example the existing RESTBase routes /lists/setup and /lists/{id} would conflict, preventing registration. If parameter validation was integrated with path template matching, such routes could be achieved. This would come at the cost of increased complexity of the matcher, and parameter validation errors would typically lead to an unexplained 404 instead of being customisable by the handler.
- The Handler base class is going to be quite large. Not quite the kitchen sink of ApiBase (2700 lines), because substantial functionality like RequestInterface and ResponseFactory are split out, but it could easily be 500 lines. Is that OK?
- Should Handler implement IContextSource? Or provide getContext()?
- How should deserialized parameters be provided to the handler? The Action API and OpenAPI both have a concept of parameter deserialization, which occurs simultaneously with validation. Parameters are converted from strings to a specified type.