Skip to content

ContentEncodingMiddleware to decompress incoming requests #14

@boesing

Description

@boesing

Feature Request

Q A
New Feature yes
RFC yes
BC Break no

Summary

I recently found out, that request payloads usually won't be compressed, as the client does not "challenge" possible encodings with the server.
After enforcing Content-Encoding: gzip on the client side (because we are requesting an internal API), I also found out that the Webserver (NGINX in our case) is not able to decompress incoming requests so that the application only gets the decompressed content. There is no such configuration without having annoying lua scripts within NGINX and thus, I've created a middleware to handle this.

use Api\Exception\InvalidRequestPayloadException;
use Fig\Http\Message\StatusCodeInterface;
use Mezzio\ProblemDetails\ProblemDetailsResponseFactory;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\StreamInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RuntimeException;
use Safe\Exceptions\ZlibException;
use function sprintf;

final class ContentEncodingDecoderMiddleware implements MiddlewareInterface
{
    private const CONTENT_ENCODING_GZIP = 'gzip';

    /**
     * @var StreamFactoryInterface
     */
    private $streamFactory;

    /**
     * @var ProblemDetailsResponseFactory
     */
    private $responseFactory;

    public function __construct(StreamFactoryInterface $streamFactory, ProblemDetailsResponseFactory $responseFactory)
    {
        $this->streamFactory = $streamFactory;
        $this->responseFactory = $responseFactory;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if (!$request->hasHeader('Content-Encoding')) {
            return $handler->handle($request);
        }

        $encoding = mb_strtolower($request->getHeaderLine('Content-Encoding'));

        if ($encoding !== self::CONTENT_ENCODING_GZIP) {
            return $this->responseFactory->createResponse(
                $request,
                StatusCodeInterface::STATUS_NOT_ACCEPTABLE,
                sprintf(
                    'Request contains content with an unsupported encoding "%s". Only "%s" is supported!',
                    $encoding,
                    self::CONTENT_ENCODING_GZIP
                ),
                'Unacceptable Content Encoding',
                'https://docs.handyvertrag.check24.de/problem/unacceptable-content-encoding/'
            );
        }

        if ($request->getParsedBody() !== null) {
            throw new RuntimeException(
                'The request already contains a parsed body.'
                . ' Please ensure that the `BodyParamsMiddleware` comes after this middleware!'
            );
        }

        return $handler->handle($request->withBody($this->decode($request->getBody())));
    }

    private function decode(StreamInterface $payload): StreamInterface
    {
        $contents = (string) $payload;

        try {
            $decoded = \Safe\gzuncompress($contents);
        } catch (ZlibException $exception) {
            throw InvalidRequestPayloadException::create($exception);
        }

        return $this->streamFactory->createStream($decoded);
    }
}

This middleware could be modified to handle multiple Content-Encoding header values (gzip, br, e.g.).
Thus, I think we might want to have some kind of ContentDecompressionInterface like:

use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\StreamInterface;

interface ContentDecompressionInterface
{
    public function canDecompress(ServerRequestInterface $request): bool;
    public function decompress(StreamInterface $body): StreamInterface;
}

Having gzip integrated with the middleware per-default would be okay for me.
If anyone needs additional support, the interface can be used to implement all other types of encodings.


I am open to remove the dependency of the BodyParamsMiddleware as this would limit the requests to JSON/Form requests.

Validation of the request methods with an exclude list might be a good idea aswell. Thats what BodyParamsMiddleware does aswell.
Having these methods available as a constant would be something I'd like to see aswell. Maybe thats something we could contribute to fig/http-message-util @weierophinney?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions