-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add BodyParsingMiddleware #2798
Conversation
Rebased to tip of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good work. Some minor change requests.
@adriansuter Review items addressed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wonderful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor changes. Mainly just doc bloc stuff. This is a great addition to our middleware suite.
@l0gicgate Review items addressed. |
@akrabat what about adding |
A key reason that That's not the case for this one, so we'd be adding it for the dev experience of: $app->addErrorMiddleware(true, true, true);
$app->addBodyParsing(); looking nicer than: $app->addErrorMiddleware(true, true, true);
$app->add(new Slim\Middleware\BodyParsingMiddleware()); I'm easy either way and defer to @l0gicgate on it. |
Before this is merged, I would like to give a more complete review of it. I think it can be improved and be made more friendly to IoC. |
} | ||
|
||
if (isset($this->bodyParsers[$mediaType])) { | ||
$body = (string)$request->getBody(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the stream is not seekable (rewindable)?
This would be a problem if any subsequent middleware, for whatever reason, needed to access the raw body.
An example would be authentication via HMAC.
One solution would be to simply copy the non-seekable stream to a new seekable stream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if the stream is not seekable (rewindable)?
This would be a problem if any subsequent middleware, for whatever reason, needed to access the raw body.
An example would be authentication via HMAC.
__toString()
will rewind (if it can) and leaves the pointer back where it was before the call, so the stream won't look any different for subsequent middleware. (If it can't rewind, then nothing's changed of course.)
One solution would be to simply copy the non-seekable stream to a new seekable stream.
If the stream is not rewindable, then there's no guarantee that the data at the beginning is anywhere in memory now, so I don't see any advantage to copying to seekable stream?
At the end of the day, I think that if you're using a non-rewindable string in your ServerRequest, then you're incompatible with getParsedBody() and won't need or want this middleware.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
__toString()
will rewind (if it can) and leaves the pointer back where it was before the call, so the stream won't look any different for subsequent middleware. (If it can't rewind, then nothing's changed of course.)
According to PSR-7 it is not a requirement to attempt to rewind the stream after the data has been read by __to_string()
. PSR-7 only requires the stream to be rewound before the data is read.
If the stream is not rewindable, then there's no guarantee that the data at the beginning is anywhere in memory now, so I don't see any advantage to copying to seekable stream?
At the end of the day, I think that if you're using a non-rewindable string in your ServerRequest, then you're incompatible with getParsedBody() and won't need or want this middleware.
Well most often you will see something like $streamFactory->createStreamFromFile('php://input')
when creating the body stream.
The problem is that there is no guarantee php://input
is seekable.
So the only real solution if the stream is not seekable and the raw data needs to access more than once is to copy the stream.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well most often you will see something like
$streamFactory->createStreamFromFile('php://input')
when creating the body stream.
The problem is that there is no guaranteephp://input
is seekable.
So the only real solution if the stream is not seekable and the raw data needs to access more than once is to copy the stream.
Right. I see what you're saying now. The problem is that we can't copy the stream as there's no method in StreamInterface
that provide access to the underlying stream object.
Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A further note. It looks like this has to be solved at the PSR-7 implementation level. e.g. Diactoros's PhpInputStream which is then used by ServerRequest.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anfly0 I'm confused trying to do this.
If the stream is not seeable, then we can't rewind it and hence can't parse it in this middleware. Have I missed something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So maybe I copy the stream if it's not seekable and $stream->tell()
is 0
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@akrabat I'm no 100 on this, but I think that $stream->tell()
can return 0 on a not seekable stream even if some of the data has been read.
Offsets have no real meaning if the stream is not seekable.
One idea is to copy the stream if it's not seekable and then compare the size of the new stream with what's in the Content-Length header.
If the match it's reasonably safe to assume that all data was copied and if they don't some error handling can be done.
And then parse the contents.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we get a non-seeable stream into a ServerRequest
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure I understand the question.
The PSR-7 implementation used is free to use whatever resource or method to create the stream.
It is an edge case, but it is entirely possible for the stream not to be seekable.
I'd maybe rename that to I mean theoretically we should probably do a helper for all internal middleware:
What do you think @adriansuter |
As long as there aren't dozens of internal middleware, why not. This would make things definitely more developer friendly. But we need to pay attention not to blow up our Unfortunately this works only for internal middleware. So we would mix two "styles" of adding middleware. I'm not too happy with that, as I would prefer to make things consistent. But that is some sort of compromise, I guess. |
Why not basic auth, cors, trailing slash, http cache or gzip/deflate encoding middlewares? I mean routing and error must of course be bundled with Slim but apart from those, there are already great PSR15 middleware out there for all those things I was surprised yesterday when I saw this PR, I guess it is because you normally receive questions about how to support X payload, but that can be documented Just my thoughts if they are of any use |
The ones that @l0gicgate listed all functionality that was built into Slim 3. With Slim 4 we have moved them into separate components so that the user can now choose to replace with more appropriate solutions for their use-case.
This one has come about because the solution we thought we had with Slim-Http does't work as well as we'd like with PSR-15, so this PR addresses that gap. I don't think we're against supplying other middleware as optional components if we feel that it's something that Slim should come with out of the box and there's not a wide-used, simple-to-use option out there already. |
I'm hoping that this will be it for our internal middleware suite. I like consistency as well. So with that line of thinking I guess we should add helper methods for all of the internal middleware. We can do that in a different PR though. |
I've added App::addBodyParsingMiddleware() to this PR. I agree that separate PRs for the other middleware makes sense. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As @juliangut says, middlewares/payload already exists and works for any PSR-15 system.
At minimum, I feel like this kind of solution would be better published as a separate package (or additional functionality contributed to middlewares/payload
) that anyone can use with PSR-15 dispatchers. To me this largely feels like "not invented here" and, while pretty easy to use for simple applications, is not particularly flexible.
* @param callable $callable A callable that returns parsed contents for media type. | ||
* @return self | ||
*/ | ||
public function registerBodyParser(string $mediaType, callable $callable): self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than a generic callable
, why not create an interface?
interface BodyParser
{
public function canParse(string $contentType): bool;
/**
* @return mixed
*/
public function parse(StreamInterface $stream);
}
Then you can do:
$middleware = new BodyParsingMiddleware();
$middleware->register(new JsonParser());
$middleware->register(new XmlParser());
// etc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't disagree with you on this. This to me feels like it should be extracted in its own package if we're going to go that route so we can contain the files contextually. The middleware we ship right now are self-contained into one file.
|
||
protected function registerDefaultBodyParsers(): void | ||
{ | ||
$this->registerBodyParser('application/json', function ($input) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine for basic applications, but what about users of JSON-API that use application/vnd.api+json
as the content type? The parsing is exactly the same but this hard lock on application/json
won't work.
// If not, look for a media type with a structured syntax suffix (RFC 6839) | ||
$parts = explode('+', $mediaType); | ||
if (count($parts) >= 2) { | ||
$mediaType = 'application/' . $parts[count($parts) - 1]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I get that this is supposed to handle application/vnd.api+json
using application/json
parser... that behavior is not particularly obvious though, and would be better (IMO) handled on a case by case basis in a specific parser.
This middleware will parse the body of a PSR-7 ServerRequest and if the content-type is known and a parser is registered, it will add the parsed data to the request object before passing to the next middleware. Usage: use Slim\Factory\AppFactory; use Slim\Middleware\BodyParsingMiddleware; use Slim\Psr7\Response; $app = AppFactory::create(); $app->add(new BodyParsingMiddleware()); $app->post('/foo', function ($request) { $data = $request->getParsedBody(); $response = new Response(); $response->getBody()->write( print_r($data, true) ); return new Response(); }); $app->run();
This follows the same style as addRoutingMiddleware() and addErrorMiddleware() in order to have consistent developer experience when adding a built-in Slim middleware component.
@l0gicgate I'm happy with this as the first cut. The only potential outstanding issue is related to non-seekable streams and I'd like to see a real use-case for a non-seeable stream in a |
Are we supposed to use |
@esetnik no need to use it no. And we are not removing it as you may use Slim-Http without using Slim |
It is beneficial to have it in Slim-Http to help migration from Slim 3. |
This middleware will parse the body of a PSR-7 ServerRequest and if the content-type is known and a parser is registered, it will add the parsed data to the request object before passing to the next middleware.
Usage: