Fosrestbundle body empty when multipart request - php

In the code bellow I expect the $request->getContents() to get the body content of the HTTP request. When sending non multipart request this works as expected though when using multipart requests the $body variable remains empty.
public function postDebugAction(Request $request) {
$body = $request->getContent();
if (empty($body)) {
throw new \Exception('Body empty.');
}
return $this->view(array(), 201);
}
After reading this question and answer I added a body listener aswell.
<?php
namespace VSmart\ApiBundle\Listener;
use FOS\RestBundle\EventListener\BodyListener as BaseBodyListener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use FOS\RestBundle\Decoder\DecoderProviderInterface;
class BodyListener extends BaseBodyListener {
/**
* #var DecoderProviderInterface
*/
private $decoderProvider;
/**
* #param DecoderProviderInterface $decoderProvider Provider for fetching decoders
*/
public function __construct(DecoderProviderInterface $decoderProvider) {
$this->decoderProvider = $decoderProvider;
}
/**
* {#inheritdoc}
*/
public function onKernelRequest(GetResponseEvent $event) {
$request = $event->getRequest();
if (strpos($request->headers->get('Content-Type'), 'multipart/form-data') !== 0) {
return;
}
$format = 'json';
if (!$this->decoderProvider->supports($format)) {
return;
}
$decoder = $this->decoderProvider->getDecoder($format);
$iterator = $request->request->getIterator();
$request->request->set($iterator->key(), $decoder->decode($iterator->current(), $format));
}
}
According to my PHPUnit test this was working though when using Postman and Advanced Rest Client to simulate the request the body seems to be empty again. I double checked this to run both the simulate requests as PHPUnit with the debugger. Result is that, indeed, the body is empty when simulated via a Rest client and not empty when ran through PHPUnit.
The test case I used:
POST url:
http://localhost/EntisServer/web/app_dev.php/api2/debug
Headers:
Authorization: Bearer ZGYzYjY1YzY4MGY3YWM3OTFhYTI4Njk3ZmI0NmNmOWZmMjg5MDFkYzJmOWZkOWE4ZTkyYTRmMGM4NTE1MWM0Nw
Content-Type: multipart/form-data; boundary=-----XXXXX
Content:
-----XXXXX
Content-Disposition: form-data; name="json"
Content-Type: application/json; charset=utf-8
{
"blabla": 11
}
-----XXXXX
Content-Disposition: form-data; name="q_3101"; filename="image.jpg"
Content-Type: image/jpeg
contents of a file...
-----XXXXX--
UPDATE
I was uncertain whether I stepped through the debugger without using the BodyListener. When I did the result is exactly the same. So, without the BodyListener the PHPUnit case gets the body though the simulated request is still empty.

See php:// wrappers on php.net:
Note: Prior to PHP 5.6, a stream opened with php://input could only be read once; the stream did not support seek operations. However, depending on the SAPI implementation, it may be possible to open another php://input stream and restart reading. This is only possible if the request body data has been saved. Typically, this is the case for POST requests, but not other request methods, such as PUT or PROPFIND.
So update your PHP version or make sure you only read the input once.

You can find your uploaded files in $request->files->all() after fos_rest.decoder_provider decoding.

Related

Symfony run code after response was sent

I took a look at this other question. I am looking for a way to do what the OP of that question wants as well, and that is to continue processing php after sending http response, but in Symfony2.
I implemented an event that fires after every kernel termination. So far so good, but what I want is for it to fire after CERTAIN terminations, in specific controller actions, for instance after a form was sent, not every single time at every request. That is because I want to do some heavy tasks at certain times and don't want the end user to wait for the page to load.
Any idea how can I do that?
<?php
namespace MedAppBundle\Event;
use JMS\DiExtraBundle\Annotation\InjectParams;
use JMS\DiExtraBundle\Annotation\Service;
use JMS\DiExtraBundle\Annotation\Tag;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use JMS\DiExtraBundle\Annotation\Inject;
/**
* Class MedicListener
* #package MedAppBundle\EventListener
* #Service("medapp_test.listener")
* #Tag(name="kernel.event_subscriber")
*/
class TestListener implements EventSubscriberInterface
{
private $container;
private $logger;
/**
* Constructor.
*
* #param ContainerInterface $container A ContainerInterface instance
* #param LoggerInterface $logger A LoggerInterface instance
* #InjectParams({
* "container" = #Inject("service_container"),
* "logger" = #Inject("logger")
* })
*/
public function __construct(ContainerInterface $container, LoggerInterface $logger = null)
{
$this->container = $container;
$this->logger = $logger;
}
public function onTerminate()
{
$this->logger->notice('fired');
}
public static function getSubscribedEvents()
{
$listeners = array(KernelEvents::TERMINATE => 'onTerminate');
if (class_exists('Symfony\Component\Console\ConsoleEvents')) {
$listeners[ConsoleEvents::TERMINATE] = 'onTerminate';
}
return $listeners;
}
}
So far I've subscribed the event to the kernel.terminate one, but obviously this fires it at each request. I made it similar to Swiftmailer's EmailSenderListener
It feels kind of strange that the kernel must listen each time for this event even when it's not triggered. I'd rather have it fired only when needed, but not sure how to do that.
In the onTerminate callback you get an instance of PostResponseEvent as first parameter. You can get the Request as well as the Response from that object.
Then you should be able to decide if you want to run the actual termination code.
Also you can store custom data in the attributes bag of the Request. See this link: Symfony and HTTP Fundamentals
The Request class also has a public attributes property, which holds special data related to how the application works internally. For the Symfony Framework, the attributes holds the values returned by the matched route, like _controller, id (if you have an {id} wildcard), and even the name of the matched route (_route). The attributes property exists entirely to be a place where you can prepare and store context-specific information about the request.
Your code could look something like this:
// ...
class TestListener implements EventSubscriberInterface
{
// ...
public function onTerminate(PostResponseEvent $event)
{
$request = $event->getRequest();
if ($request->attributes->get('_route') == 'some_route_name') {
// do stuff
}
}
// ...
}
Edit:
The kernel.terminate event is designed to run after the response is sent. But the symfony documentation is saying the following (taken from here):
Internally, the HttpKernel makes use of the fastcgi_finish_request PHP function. This means that at the moment, only the PHP FPM server API is able to send a response to the client while the server's PHP process still performs some tasks. With all other server APIs, listeners to kernel.terminate are still executed, but the response is not sent to the client until they are all completed.
Edit 2:
To use the solution from here, you could either directly edit the web/app.php file to add it there (but this is some kind of "hacking core" imo, even though it would be easier to use than the following). Or you could do it like this:
Add a listener to kernel.request event with a high priority and start output buffering (ob_start).
Add a listener to kernel.response and add the header values to the response.
Add another listener with highest priority to kernel.terminate and do the flushing (ob_flush, flush).
Run your code in a separate listener with lower priority to kernel.terminate
I did not try it, but it should actually work.
To solve this issue for some of my use cases I simply create symfony commands to do the heavy tasks, and call them via exec() to make them run in a separate process.
I used these answers to write a Response class that has this functionality:
https://stackoverflow.com/a/28738208/1153227
This implementation will work on Apache and not just PHP FPM. However, to make this work we must prevent Apache from using gzip (by using an invalid Content-Encoding) so it makes sense to have a custom Response class to specify exactly when having an early response is more important than compression.
use Symfony\Component\HttpFoundation\Response;
class EarlyResponse extends Response
{
// Functionality adapted from this answer: https://stackoverflow.com/a/7120170/1153227
protected $callback = null;
/**
* Constructor.
*
* #param mixed $content The response content, see setContent()
* #param int $status The response status code
* #param array $headers An array of response headers
*
* #throws \InvalidArgumentException When the HTTP status code is not valid
*/
public function __construct($content = '', $status = 200, $headers = array(), $callback = null)
{
if (null !== $callback) {
$this->setTerminateCallback($callback);
}
parent::__construct($content, $status, $headers);
}
/**
* Sets the PHP callback associated with this Response.
* It will be called after the terminate events fire and thus after we've sent our response and closed the connection
*
* #param callable $callback A valid PHP callback
*
* #throws \LogicException
*/
public function setTerminateCallback($callback)
{
//Copied From Symfony\Component\HttpFoundation\StreamedResponse
if (!is_callable($callback)) {
throw new \LogicException('The Response callback must be a valid PHP callable.');
}
$this->callback = $callback;
}
/**
* #return Current_Class_Name
*/
public function send() {
if (function_exists('fastcgi_finish_request') || 'cli' === PHP_SAPI) { // we don't need the hack when using fast CGI
return parent::send();
}
ignore_user_abort(true);//prevent apache killing the process
if (!ob_get_level()) { // Check if an ob buffer exists already.
ob_start();//start the output buffer
}
$this->sendContent(); //Send the content to the buffer
static::closeOutputBuffers(1, true); //flush all but the last ob buffer level
$this->headers->set('Content-Length', ob_get_length()); // Set the content length using the last ob buffer level
$this->headers->set('Connection', 'close'); // Close the Connection
$this->headers->set('Content-Encoding', 'none');// This invalid header value will make Apache not delay sending the response while it is
// See: https://serverfault.com/questions/844526/apache-2-4-7-ignores-response-header-content-encoding-identity-instead-respect
$this->sendHeaders(); //Now that we have the headers, we can send them (which will avoid the ob buffers)
static::closeOutputBuffers(0, true); //flush the last ob buffer level
flush(); // After we flush the OB buffer to the normal buffer, we still need to send the normal buffer to output
session_write_close();//close session file on server side to avoid blocking other requests
return $this;
}
/**
* #return Current_Class_Name
*/
public function callTerminateCallback() {
if ($this->callback) {
call_user_func($this->callback);
}
return $this;
}
}
You also need to add a method to your AppKernel.php to make this work (don't forget to add a use statement for your EarlyResponse class)
public function terminate(Request $request, Response $response)
{
ob_start();
//Run this stuff before the terminate events
if ($response instanceof EarlyResponse) {
$response->callTerminateCallback();
}
//Trigger the terminate events
parent::terminate($request, $response);
//Optionally, we can output the beffer that will get cleaned to a file before discarding its contents
//file_put_contents('/tmp/process.log', ob_get_contents());
ob_end_clean();
}

Guzzle 6: no more json() method for responses

Previously in Guzzle 5.3:
$response = $client->get('http://httpbin.org/get');
$array = $response->json(); // Yoohoo
var_dump($array[0]['origin']);
I could easily get a PHP array from a JSON response. Now In Guzzle 6, I don't know how to do. There seems to be no json() method anymore. I (quickly) read the doc from the latest version and don't found anything about JSON responses. I think I missed something, maybe there is a new concept that I don't understand (or maybe I did not read correctly).
Is this (below) new way the only way?
$response = $client->get('http://httpbin.org/get');
$array = json_decode($response->getBody()->getContents(), true); // :'(
var_dump($array[0]['origin']);
Or is there an helper or something like that?
I use json_decode($response->getBody()) now instead of $response->json().
I suspect this might be a casualty of PSR-7 compliance.
You switch to:
json_decode($response->getBody(), true)
Instead of the other comment if you want it to work exactly as before in order to get arrays instead of objects.
I use $response->getBody()->getContents() to get JSON from response.
Guzzle version 6.3.0.
If you guys still interested, here is my workaround based on Guzzle middleware feature:
Create JsonAwaraResponse that will decode JSON response by Content-Type HTTP header, if not - it will act as standard Guzzle Response:
<?php
namespace GuzzleHttp\Psr7;
class JsonAwareResponse extends Response
{
/**
* Cache for performance
* #var array
*/
private $json;
public function getBody()
{
if ($this->json) {
return $this->json;
}
// get parent Body stream
$body = parent::getBody();
// if JSON HTTP header detected - then decode
if (false !== strpos($this->getHeaderLine('Content-Type'), 'application/json')) {
return $this->json = \json_decode($body, true);
}
return $body;
}
}
Create Middleware which going to replace Guzzle PSR-7 responses with above Response implementation:
<?php
$client = new \GuzzleHttp\Client();
/** #var HandlerStack $handler */
$handler = $client->getConfig('handler');
$handler->push(\GuzzleHttp\Middleware::mapResponse(function (\Psr\Http\Message\ResponseInterface $response) {
return new \GuzzleHttp\Psr7\JsonAwareResponse(
$response->getStatusCode(),
$response->getHeaders(),
$response->getBody(),
$response->getProtocolVersion(),
$response->getReasonPhrase()
);
}), 'json_decode_middleware');
After this to retrieve JSON as PHP native array use Guzzle as always:
$jsonArray = $client->get('http://httpbin.org/headers')->getBody();
Tested with guzzlehttp/guzzle 6.3.3
$response is instance of PSR-7 ResponseInterface. For more details see https://www.php-fig.org/psr/psr-7/#3-interfaces
getBody() returns StreamInterface:
/**
* Gets the body of the message.
*
* #return StreamInterface Returns the body as a stream.
*/
public function getBody();
StreamInterface implements __toString() which does
Reads all data from the stream into a string, from the beginning to end.
Therefore, to read body as string, you have to cast it to string:
$stringBody = $response->getBody()->__toString()
Gotchas
json_decode($response->getBody() is not the best solution as it magically casts stream into string for you. json_decode() requires string as 1st argument.
Don't use $response->getBody()->getContents() unless you know what you're doing. If you read documentation for getContents(), it says: Returns the remaining contents in a string. Therefore, calling getContents() reads the rest of the stream and calling it again returns nothing because stream is already at the end. You'd have to rewind the stream between those calls.
Adding ->getContents() doesn't return jSON response, instead it returns as text.
You can simply use json_decode

FosRestbundle force content-type for error messages

I'm looking for a simple, stupid solution to force the content-type to application/json for all symfony2 http error messages in fosrestbundle (like MethodNotAllowedHttpException etc.).
Example request headers:
Content-Type: application/x-www-form-urlencoded
Accept: */*
Current response headers (MethodNotAllowedHttpException):
Content-Type: text/html; charset=UTF-8
you can throw the error if the value in the header fails your logic test. then in your catch statement, return a json response. something like this (untested code)
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
// .....
public function myAction(Request $request){
try{
// ...
$cType = $request->headers->get('Content-Type');
// logic for testing if the content type is allowed
if(!in_array($cType,$allowedArray)){
throw new MethodNotAllowedHttpException();
}
// .....
}catch(\Exception $e){
if(get_class($e) == "MethodNotAllowedHttpException"){
$data = array("success"=>false,"message"=>"Method not allowed")
return new JsonResponse($data);
}
}
}
This way you can handle different exceptions in different ways. I'm not sure if it was the content type that you want to use to determine if you throw the exception or not, but you can get any header info by using $request->headers->get()

Zend_Form Failing validation with Optional File in IE10

TL;DR: Zend_Form_Element_File is not playing nice with IE10
Alright, bear with me while I unravel this tale of one of the worst bugs I've ever encountered. (Only applies to IE10)
I'm using a Zend_Form (Zend Framework 1.12) with a Zend_Form_Element_File:
$file = (new Zend_Form_Element_File('file'))
->setRequired(false);
I'm also using jQuery Form Plugin to use AJAX or an iFrame when appropriate. (Which is a new development, previously I was only using an iframe [and this bug was found in that version] and I since moved the iframe to be XHR2 Feature Detected).
So we have this form that AJAXly submits the file and the other variables to the server, which tries to validate it through Zend_Form. No big deal. Chrome and Firefox send empty files which Zend detects and goes no problem, and IE was sending nothing related to the file, and is now sending an empty parameter named file (NOT an empty file) and Zend_Form is saying that the "file is too big."
The files array is empty, So I implemented the patch suggested on Zend Issue ZF-12189 to get:
$check = $this->_getFiles($files, false, true);
if (empty($check)) {
if ($this->_options['ignoreNoFile']) {
return true;
}
return false;
}
but as $check is not evaluating as empty the problem persists.
Relevant Request Headers:
X-Requested-With: XMLHttpRequest
Accept: text/html, */*; q=0.01
Content-Type: multipart/form-data; boundary=---------------------------7dd299161d06c6
Content-Length: 580
Request Body:
-----------------------------7dd299161d06c6
Content-Disposition: form-data; name="entryId"
9
-----------------------------7dd299161d06c6
Content-Disposition: form-data; name="csrf"
b9774f3998695465d9b3079eb028e342
-----------------------------7dd299161d06c6
Content-Disposition: form-data; name="description"
test
-----------------------------7dd299161d06c6
Content-Disposition: form-data; name="MAX_FILE_SIZE"
2097152
-----------------------------7dd299161d06c6
Content-Disposition: form-data; name="file"
-----------------------------7dd299161d06c6--
Form Messages:
{"file":{"fileUploadErrorIniSize":"File 'file' exceeds the defined ini size"}}
Does anyone know of a solution to this problem?
Here is a workaround you can add to override the Zend_Form's isValid() method. There is a legitimate bug out there that generates the same error when no files are uploaded yet it still attempts to validate anyway. Maybe this will help someone out there.
public function isValid($data) {
$valid = parent::isValid($data);
$errorCount = 0;
foreach($this->getElements() as $elem) {
if ($elem->hasErrors()) {
$errorCount++;
}
// Related issues:
// http://framework.zend.com/issues/browse/ZF-12159
// http://framework.zend.com/issues/browse/ZF-10279
// http://framework.zend.com/issues/browse/ZF-12189
if ($elem instanceof Zend_Form_Element_File && !$elem->isRequired() && !isset($_FILES[$elem->getName()]) && $elem->hasErrors()) {
$elem->clearErrorMessages();
$elem->setTransferAdapter( 'Http' ); // reset any errors that may be on the transfer adapter
$errorCount--;
}
}
if ($this->_errorsForced) {
return false;
}
if ($errorCount==0) {
$this->_errorsExist = false;
}
return $errorCount==0;
}
you may want to see this issue: Form Bug in JS
When we ran into this, we just forced iFrame: true and that did the trick. It made me a little sad, but it works. :)

Access-Control-Allow-Origin is not showing up in response headers from codeigniter

My Codeigniter file says
$CI->output->set_header("Access-Control-Allow-Origin: *");
$CI->output->set_header("Access-Control-Expose-Headers: Access-Control-Allow-Origin");
$CI->output->set_status_header(200);
$CI->output->set_content_type('application/json');
echo json_encode(array("city" => "dhaka"));
but the http response that i get are:
Request URL:http://localhost/index.php/location/city
Request Method:POST
Status Code:200 OK
Connection:Keep-Alive
Content-Length:16
Content-Type:text/html
Date:Sun, 22 Jul 2012 10:27:32 GMT
Keep-Alive:timeout=5, max=100
Server:Apache/2.2.21 (Unix) mod_ssl/2.2.21 OpenSSL/0.9.8r DAV/2 PHP/5.3.6
X-Powered-By:PHP/5.3.6
The header Access-Control-Allow-Origin is missing in the response even after including Access-Control-Expose-Headers: Access-Control-Allow-Origin. My source of information about this header is from Mozilla Developer Website
It turns out, it worked for me only when i set the headers via the PHP syntax header() instead of the codeigniter syntax $CI->output->set_header(). That's sad.
Thanks to the first comment by #Yan at the Question of this topic
If you look closely you can also notice the content-type being different: it's text/html, whereas you are requesting application/json. This happens because while you are preparing the headers correctly, you never actually output them. As far as I know you can do this in at least 2 ways:
Use the output library's set_output function to output everything at once.
$json = json_encode(array("city" => "dhaka"));
$this->output->set_header("Access-Control-Allow-Origin: *");
$this->output->set_header("Access-Control-Expose-Headers: Access-Control-Allow-Origin");
$this->output->set_status_header(200);
$this->output->set_content_type('application/json');
$this->output->set_output($json);
Call the output-library's _display() function, to first output the correct headers and then append your json object with echo.
$this->output->set_header("Access-Control-Allow-Origin: *");
$this->output->set_header("Access-Control-Expose-Headers: Access-Control-Allow-Origin");
$this->output->set_status_header(200);
$this->output->set_content_type('application/json');
$this->output->_display();
echo json_encode(array("city" => "dhaka"));
This function sends the finalized output data to the browser along with any server headers and profile data. (From CI/system/core/Output.php line 316)
after some digging around, i found that $CI->output->set_header() does work - when there isn't an error or exception.
When there is an error or exception that CI can catch, the output & view classes are bypassed completely and the appropriate error pages are rendered with include(VIEWPATH.'errors/'.$template.'.php') and headers sent with set_status_header($status_code) (located at <CI System Dir>/core/Common.php)
see <CI System Dir>/core/Exceptions.php
here's a sample:
/**
* General Error Page
*
* Takes an error message as input (either as a string or an array)
* and displays it using the specified template.
*
* #param string $heading Page heading
* #param string|string[] $message Error message
* #param string $template Template name
* #param int $status_code (default: 500)
*
* #return string Error page output
*/
public function show_error($heading, $message, $template = 'error_general', $status_code = 500)
{
set_status_header($status_code);
$message = '<p>'.implode('</p><p>', is_array($message) ? $message : array($message)).'</p>';
if (ob_get_level() > $this->ob_level + 1)
{
ob_end_flush();
}
ob_start();
include(VIEWPATH.'errors/'.$template.'.php');
$buffer = ob_get_contents();
ob_end_clean();
return $buffer;
}
it's annoying in that it makes DRY less straight forward. to work around it, i suggest you create a helper function, for example (untested):
function my_generate_headers($headers=array(),$useOutputClass=true)
{
if(is_array($headers) && count($headers)<1) return false;
foreach($headers AS $eHeader)
{
($useOutputClass) ?
get_instance()->output->set_header('X-Powered-By: C-C-C-Cocaine') :
#header('X-Powered-By: Errors',true);
}
return true;
}
use that function in your different error pages at <CI Views>/errors/error_*.php as well as in your controllers.
What worked for me is:
$this->output
->set_header('Access-Control-Allow-Origin: http://localhost:4567')
->set_header('Content-type: application/json')
->set_status_header(200)
->set_output( json_encode($to_encode) )
->_display();

Categories