I am testing POSTing data to an API endpoint we've created using Laravel 5.2, but none of the parameters seem to be reaching the application in the test. The endpoint expects json and responds with json and uses a FormRequestValidator which has required rules for active and make parameters. The test fails with status code 422 and the examining the response body it states the active and make parameters are required even though they are being passed in the call so therefore for some reason when the request reaches the the Form Request Validator, the input is not there.
However, when I invoke the endpoint with json body including make and active from Postman or the UI app we've built it works fine, it is only failing in the PHPUnit tests therefore it must be something with PHPUnit or the test setup being incorrect. Here is the test:
public function testItStoresCars()
{
// Arrange
$user = User::first();
//Act
$this->json(Request::METHOD_POST, '/v1/cars', [
'active' => true,
'make' => 'Audi'
],
['Authorization' => 'Bearer '.\JWT::fromUser($user)]));
// Assert
$this->assertResponseOk();
}
I know the Authorisation header is set correctly from other tests passing.
I've tried disabling middleware, using the $this->post helper method and manually setting the headers as well as using the $this->call method with setting the Headers and encoding the data using json_encode but I always get the same 422 response. I'm wondering has anyone encountered this issue or can see an error?
Controller Code
public function store(CreateCarRequest $request)
{
$car = $this->carRepo->save($request->all());
return response()->json(car);
}
FormRequest
class CreateCarRequest extends Request
{
public function rules()
{
return [
'active' => 'required|boolean',
'make' => 'required',
];
}
}
422 is the error response for validation errors.. which means either your data is not posted or it doesn't pass server validation, try
$this->json(Request::METHOD_POST, '/v1/cars', [
'active' => true,
'make' => 'Audi'
],
['Authorization' => 'Bearer '.\JWT::fromUser($user)]))->dump();
dump() should show you the errors
Related
I have a simple route in Laravel 8 to return some request data. But when I send the request in Postman with POST selected, I get an error of "The GET method is not supported for this route." Keep in mind, I have POST selected in Postman, not GET.
Here is the route:
Route::post('post-route', 'UserController#postFunction');
Here is is the function being called in UserController:
public function postFunction(Request $request) {
return [
'id1' => $request->id1,
'id2' => $request->id2,
];
}
In Postman I am passing the data as json:
{
'id1': 1234,
'id2': 4321
}
I am simply trying to make sure I am passing the correct data in the request but I am getting this error. Why is it trying to hit a GET request?
You can't test your POST, PUT or DELETE routes with Postman because Laravel uses the CSRF middleware protection.
If you really want to use Postman, you need to comment it to disable this middleware temporarly in app/Http/Kernel.php:
protected $middlewareGroups = [
'web' => [
(...)
//\App\Http\Middleware\VerifyCsrfToken::class,
],
(...)
];
But don't forget to enable it again once you want to deploy your project in production!
If you don't want to disable temporarly the CSRF middleware, you can follow the steps mentioned here https://gist.github.com/ethanstenis/3cc78c1d097680ac7ef0, but it's a little longer.
I am consuming an API which receives a POST request with some parameters and uses them to generate a file in S3. This API returns a 303 redirect response with Location: set to the signed URL to access the S3 file.
This works file when accessing the API via e.g. Postman however when accessing it via GuzzleHttp (v7.4) it fails with error SignatureDoesNotMatch.
While debugging I have used code:
$client = new Client([
'allow_redirects' => true,
'on_stats' => function (TransferStats $stats) {
var_dump($stats->getEffectiveUri());
}
]);
$client->post('https://api.example.com', [
'json' => [ ... ]
]);
Doing this I have confirmed that the URL is correct and copy/pasting the exact url that is accessed actually works. However using GuzzleHttp it does not work at all.
Update: The API developer has informed me that this issue was also because they were using v2 of the AWS S3 signature. They have now changed it to v4 which makes my code work as is (I think they may have had the same issues from other clients).
The issue turned out to be that when Guzzle follows redirects, it retains most of the original request headers. However the Amazon computed signature also validates the signature against (at least some of) the headers. The offending header was the Content-Type header which was still sent even though the request no longer had any content.
To fix this I created a new Guzzle middleware:
use Psr\Http\Message\RequestInterface;
class RemoveContentType {
public function __invoke(callable $handler) {
return function (RequestInterface $request, array $options) use ($handler) {
if ($request->getHeaderLine('Content-Type')
&& $request->getBody()->getSize() === 0) {
return $handler($request->withoutHeader('Content-Type'), $options);
}
return $handler($request, $options);
};
}
}
this would remove the content type from a request which has an empty body.
I then modified my code to use this middleware:
$stack = HandlerStack::create();
$stack->push(new RemoveContentType ());
$client = new Client([
'handler' => $stack,
'allow_redirects' => true,
]);
$client->post('https://api.example.com', [
'json' => [ ... ]
]);
This finally solved my issue.
Environment
I created an application using Laravel 5.7 and implemented a REST API. I have a route in routes/api.php that triggers a middleware which checks if the incoming request has a parameter called api_token.
This is a production environment and here are the specifics:
Linux Ubuntu 18.04 LTS 64-bit
nginx/1.14.0
Laravel 5.7
PHP 7.2
APP_ENV in the .env file is set to 'production' and APP_DEBUG is set to 'false'.
Problem
My problem is that the incoming request object is always empty when it arrives at the server. At least that's what my Laravel application says.
These are my routes in routes/api.php:
Route::middleware('rest')->group(function() {
Route::get('device-location/{deviceID}', 'PositionDataController#getDeviceLocation');
Route::get('device-location/all', 'PositionDataController#getAllLocations');
Route::post('device-location', 'PositionDataController#storeDeviceLocation');
Route::put('device-location/{deviceID}', 'PositionDataController#updateDeviceLocation');
Route::delete('device-location/{deviceID}', 'PositionDataController#deleteDeviceLocation');
});
The routes are in a middleware group called 'rest' as you can see. I'm using the Route::get('device-location/{deviceID}', 'PositionDataController#getDeviceLocation'); route to test the functionality.
Here's the code from the middleware:
public function handle($request, Closure $next)
{
if(request()->all()) {
$deviceID = request()->device_id;
}
else {
return response()->json([
'error' => 'The request object is empty.',
'request' => request(),
'parameters' => request()->all(),
'content' => request()->getContent(),
'input' => request()->input()
], 500);
}
$device = MobileDevice::where('device_id', $deviceID)->first();
if($device) {
$deviceToken = $device->api_token;
if($deviceToken == request()->api_token) {
return $next($request);
}
else {
return response()->json(['error' => 'Token does not match.'], 500);
}
}
else {
return response()->json(['error' => 'The device with the id [' . $deviceID . '] could not be found.'], 500);
}
}
The middleware first checks if there are parameters in the request object and then does some logic to check if the right token was sent. If the request object is empty it returns some data to help me understand what went wrong.
I use Postman (https://www.getpostman.com) to test the API. Here's my Postman setup:
Postman setup
Postman headers
This is the response I get in Postman:
Postman response
I get the same result if I call that route in a browser.
Regardless of if I put in parameters or not the request seems to be always empty.
Here are the things that I've tried to do:
Not using the middleware
Using the $request variable instead of the helper request()
Switching between 'application/json' and 'application/x-www-form-urlencoded' in the Headers of my Postman setup
Calling that route in a browser
Updating to Laravel 5.7
The strange thing is that all of this works perfectly on my local environment and on a test server that has the same specs as the production server.
UPDATE 01:
So it seems to be even worse...
If I add a route like this in web.php:
Route::get('/request-return', function() {
return request()->all();
});
and visit that route like this:
laravel.application/request-return?device_id=1&api_token=XgkQLs8G7OYTqdwsXmjJT5G9bxv20DRi
I get back an empty array [].
So it seems like the parameters don't get to the server itself.
You are getting device id through GET request so use the below line instead of $request()->device_id.
Use this and let me know
$name = $request->input('device_id')
Okay I could solve the problem. It didn't have anything to do with Laravel. It was a nginx configuration problem:
https://serverfault.com/questions/231578/nginx-php-fpm-where-are-my-get-params
I created an API (store) that saves the data on the database and returns 201 if successful or 404 if not.
if ($visit->save()){
$visit->view_visit = [
'href' => 'api/v1/visit/' . $visit->id,
'method' => 'GET'
];
$response = [
'msg' => 'Visit created.',
'visit' => $visit
];
return response()->json($response, 201);
}
$response = [
'msg' => 'Error during creation.'
];
return response()->json($response, 404);
It works perfectly. Using postman you can see that the status will be <<201 Created>>.
This API should be used in two ways: called by another application or called by a Laravel form. This is the question:
How do I call it in a way if it successful, it will load a given view on the browsers?
In other words, is there a way to make the form call a route (the api itself, something like ../api/visit/) and in case of success loads the other view? Also, I would like to pass the content of response['msg'] to this new view.
I know I could do it inside the store method by filtering the HTTP referrer, but I would like to keep the controller code strictly to manage the record creation. Besides that, I have to send the 201/404 codes along with the returned data.
I also considered creating another controller to handle the API response and then call the form, but it still sounds too much -- it's supposed to be easy, I guess.
In laravel you can use a helpful method which determines if the request that has been sent is an AJAX request or just a normal request, which is:
$request->wantsJson()
So, Inside your controller in the return function, you will make an if statement:
if ($request->wantsJson()) {
return response()->json();
}else{
return view(...);
}
Currently I am using the following initialization code on my api module
public function init()
{
parent::init();
Yii::$app->response->format = Response::FORMAT_JSON;
}
My api gives back response in XML format in the following example.
public function actionTest()
{
$items = ['one', 'two', 'three' => ['a', 'b', 'c']];
return $items;
}
This is the response:
<response>
<item>one</item>
<item>two</item>
<three>
<item>a</item>
<item>b</item>
<item>c</item>
</three>
</response>
The only way I can get it to work is by adding this line to each of the controller behaviors. I have read the documentation which says I can initalize this on the module class so I do not need to do it in every controller. I do not know why it gives XML though. Also just in case the only way is to add it to my behaviors, do I have to write code to handle name, code, status, type, previous and code or does Yii provide yii\rest\Controller and yii\rest\ActiveController which automatically handle this. Clearly they are being output automatically when there is an error.
{"name":"Not Found"
"message":"Page not found.",
"code":0,
"status":404
"type":"yii\\web\\NotFoundHttpException"
"previous":{"name":"Invalid Route","message":"Unable to resolve the request: api/home/",
"code":0,"type":"yii\\base\\InvalidRouteException"
}
}
After three painful days, I have found the solutions. It is sometimes quite difficult to explain this problem when you are coming from a whole JSON world of ExpressJS and NodeJS. Logically what Yii2 does is perfectly fine, on the other hand 90% RESTful APIs expect the output to be in JSON hence you do not explicitly want to set request headers every time you make an API call.
Browsers by default add request headers as "Application/XML" hence what you see on screen is XML instead of JSON.
Yii2's content negotiator upon receipt of headers as application/xml formats your output in XML. If you make a same request using CURL or PostMan with headers as "Application/JSON" you will get the desired output.
If you wish to override this behaviour then just add the below function in your controller and include the following:-
use yii\web\Response;
use yii\helpers\ArrayHelper;
public function behaviors()
{
return ArrayHelper::merge(parent::behaviors(), [
[
'class' => 'yii\filters\ContentNegotiator',
'only' => ['view', 'index'], // in a controller
// if in a module, use the following IDs for user actions
// 'only' => ['user/view', 'user/index']
'formats' => [
'application/json' => Response::FORMAT_JSON,
],
'languages' => [
'en',
'de',
],
],
]);
}
In Yii2 applications default response type is XML (and I guess it's default for REST too). During HTTP connection both sides declares what type of data are able to send and/or receive. If this information is not passed to server default data type is send(even if you specified that it should be JSON in your app) to guarantee proper communication. If you want to receive JSON data you must add Accept: application/json header to your request. And probably you don't have to specify it in php code because Yii2 should deduct it from request headers.
You can find more explanation how it works here.
I test Your code and it's work perfectly
my controller this :
<?php
namespace backend\controllers;
use yii\rest\Controller;
use yii;
use yii\web\Response;
class TestController extends Controller{
public function init()
{
parent::init();
Yii::$app->response->format = Response::FORMAT_JSON;
}
public function actionTest(){
$items = ['one', 'two', 'three' => ['a', 'b', 'c']];
return $items;
}
}
Output :
{"0":"one","1":"two","three":["a","b","c"]}
check your namespace OR send your code !