Impossible to have a 304 response when using maxage header - caching

I am trying to cache static content, I want this content is have a lifetime of one hour and the content is public, it is the same for everyone.
I have the following code in my controller:
$response = new Response();
$response->setPublic();
$response->setMaxAge(3600);
$response->setSharedMaxAge(3600);
if ($response->isNotModified($request)) {
return $response;
}
return $this->render(
'ThemesBundle:Ad:content.html.twig',
array('context' => $context, 'block' => $block),
$response
);
But the isNotModified() function always returns false.
PS: I am using Symfony 2.0.22

You made a mistake, $response->isNotModified($request) is used only when using cache validation with a ETag or a Last-Modified test!
Here, you want to use expiration methods (with Cache-Control or Expires).
So just remove theses lines :
if ($response->isNotModified($request)) {
return $response;
}
$response->setMaxAge(3600); (and setSharedMaxAge) alone will do the job, you don't need to test anything, the framework (or client navigator) will do it for you.
The same response will be served during 3600 second without passing by the action. After 3600 seconds, the user will pass by the action anew and it will be cached for 3600 seconds, etc.
In addition, you can use #Cache annotation which simplify the read ;)

Related

How to unit test if the cookie was queued or set in Laravel

I am working on a Laravel application. I am writing integration tests for my application. I am struggling to test Cookie logic. You can see my code below.
I am overriding the login method of LoginController adding the following bit
if (auth()->user()->hasRole(User::ROLE_ADMIN)) {
$accessToken = GraphQL::login($request->get('email'), $request->get('password'));
Cookie::queue("admin_access_token", $accessToken, (60 * 60 * 24) * 60);
}
As you can see, what I am trying to do is that if the user is admin, it will log into another server and save the access token in a cookie. I am using Cookie::queue.
This is my test method.
/** #test */
public function when_login_successful_access_token_is_generated_and_stored_in_cookie_if_user_is_restaurant_admin()
{
$this->post(route('login'), [
'email' => $this->restaurantAdmin->email,
'password' => 'password'
])->assertRedirect();
dd(Cookie::get('admin_access_token'));//here, coolie value is always null
}
When I run my test, as you can see in the comment, the cookie value is always null. What is wrong with my code and how can I fix it?
I would assume that the static Cookie::get() would try to fetch the cookie in the Request life cycle on the call. Since that is gone in the test, the cookie will always be null.
There is a method for seeing if a cookie exists on a response, you could use that and see if that gives the expected results.
$this->seeCookie('admin_access_token', 'yourtoken');

Laravel REST API - returning different content types based on accept header parameter

One of my endpoints require to return a PDF file. Normally it is JSON by default.
Do you think it is good to use if ($request->header('accept') === 'application/pdf') to return the PDF file? Does it break the single responsibility?
What if I have a lot of exceptions like that? It'd be too complex to have a lot of if/else statements for a lot of methods. What would be your suggestions?
You can create middleware and apply to a single endpoint or to a group of them. This would leave everything organized and easily manageable in the future.
set header for php with header function in that api at the last of response and for frontend side check header contentType on behalf of that do your remaining stuff
You have to make a middleware and and add this to kernel.php at protected $middleware array and do this code in 'handle' function
$response = $next($request);
if ($response instanceof ClassNameFromWhichObjectIsMatch )
return $response;
// add other headers
// $response->header($key, $value);
return $response;

Laravel 5.5 Request is empty in Restful controller

I have such a route in my routes/web.php
Route::resource('/api/surveys', 'SurveyController');
As documentation says, it creates all needed routes for API. This is a function, that gets executed when I go for /api/surveys route:
public function index()
{
$request = request();
if(!$request->hasHeader('token')) {
return "No auth token found.";
}
$tokenCheck = $this->userService->isTokenValid($request->header('token'));
if($tokenCheck !== true) {
return $tokenCheck;
}
return $this->surveyService->all();
}
What it does, it checks if token header parameter is set, if not, it returns an error, if yes, it checks if token is valid and etc. if everything is OK, it should return surveys from database.
public function surveys() {
$request = \Request::create('/api/surveys', 'GET');
$request->headers->set('Accept', 'application/json');
$request->headers->set('token', \Cookie::get('token'));
$response = \Route::dispatch($request);
print_r('<pre>');
print_r($response);
print_r('</pre>');
}
I have a website, that should use that API I just created to get all survey records. I create a new request object, set header "token" with token I get from a cookie and then try to dispatch and get a response. But the problem is that everytime I get "No auth token found." error. That means $request->hasHeader('token') returns false, even tough I set it here in my request. If I print_r $request->all() in Restful controller, I get an empty array.
I tried Postman to access this API with token parameter, and it works fine in postman, but here, it seems that Request disappears while it travels to API controller.
What I did wrong here?
When you manually create a request and dispatch it, that works to get the routing to call the correct controller, however that does not affect the request that is bound in the container.
When your "fake" request is handled by the api controller, the request that it pulls out of the container is the original "real" request that was made by the user.
Instead of dispatching the route with your new request, you will need to app()->handle($request) the new request. This, however, will completely replace the original "real" request with your new "fake" request, so everything from the original request will be lost.
Having said all that, this method of consuming your own api is discouraged, even by Taylor. You can read his comment on this Github issue. So, consuming your own api like this may work, but you may also run into some other unforeseen issues.
The more appropriate solution would be to extract out the logic called by the api routes to another class, and then call that extracted logic from both your api routes and your web routes.

How to send a response from a method that is not the controller method?

I've got a Controller.php whose show($id) method is hit by a route.
public function show($id)
{
// fetch a couple attributes from the request ...
$this->checkEverythingIsOk($attributes);
// ... return the requested resource.
return $response;
}
Now, in checkEverythingIsOk(), I perform some validation and authorization stuff. These checks are common to several routes within the same controller, so I'd like to extract these checks and call the method everytime I need to perform the same operations.
The problem is, I'm unable to send some responses from this method:
private function checkEverythingIsOk($attributes)
{
if (checkSomething()) {
return response()->json('Something went wrong'); // this does not work - it will return, but the response won't be sent.
}
// more checks...
return response()->callAResponseMacro('Something else went wrong'); // does not work either.
dd($attributes); // this works.
abort(422); // this works too.
}
Note: Yes, I know in general one can use middleware or validation services to perform the checks before the request hits the controller, but I don't want to. I need to do it this way.
As of Laravel 5.6 you can now use for example response()->json([1])->send();.
There is no need for it to be the return value of a controller method.
Note that calling send() will not terminate the output. You may want to call exit; manually after send().
You are probably looking for this:
function checkEverythingIsOk() {
if (checkSomething()) {
return Response::json('Something went wrong');
}
if(checkSomethingElse()) {
return Response::someMacro('Something else is wrong')
}
return null; // all is fine
}
And in the controller method:
$response = $this->checkEverythingIsOk();
if($response !== null) { // $response instanceof Response
return $response;
}
It's probably overkill, but I will throw it in anyway. You might want to look into internal requests. Also this is just pseudoish code, I have not actually done this, so take this bit of information with caution.
// build a new request
$returnEarly = Request::create('/returnearly');
// dispatch the new request
app()->handle($newRequest);
// have a route set up to catch those
Route::get('/returnearly', ...);
Now you can have a Controller sitting at the end of that route and interpret the parameters, or you use multiple routes answered by multiple Controllers/Methods ... up to you, but the approach stays the same.
UPDATE
Ok I just tried that myself, creating a new request and dispatching that, it works this way. Problem is, the execution does not stop after the child-request has exited. It goes on in the parent request. Which makes this whole approach kind of useless.
But I was thinking about another way, why not throw an Exception and catch it in an appropriate place to return a specified response?
Turns out, thats already built into Laravel:
// create intended Response
$response = Response::create(''); // or use the response() helper
// throw it, it is a Illuminate\Http\Exception\HttpResponseException
$response->throwResponse();
Now usually an Exception would be logged and you if you are in Debug mode, you would see it on screen etc. etc. But if you take a look into \Illuminate\Foundation\Exceptions\Handler within the render method you can see that it inspects the thrown Exception if it is an instance of HttpResponseException. If it is then the Response will be returned immediately.
To me the most simple and elegant way is:
response()->json($messages_array, $status_code)->throwResponse();
(you don`t need return)
It can be called from a private function or another class...
I use this in a helper class to check for permissions, and if the user doesn`t have it I throw with the above code.

Is $request->isXmlHttpRequest() reliable in symfony2?

Let's consider a scenario like below:
User selects filter button which create a AJAX call to a symfony2 controller and return the result in JSON format.
User select some other links and system will redirect him to the page
User select browser Back button.
User will see JSON response, but he should see the original page.
My controller look like below :
/**
*
*
* #Route("/ajax", name="ajax_route" , options={"expose"=true})
* #Template()
*/
public function someAction()
{
$request = $this->getRequest();
$json = array( );
if($request->isXmlHttpRequest())
{
$res = json_encode($json);
return new Response($res , 200 , array( 'Content-Type' => 'application/json' ));
}
return array( );
}
In other words, if user press back button the if($request->isXmlHttpRequest()) returns true which is not the result I am looking for. Is it a normal behavior or what ?
Symfony\Component\HttpFoundation\Request::isXmlHttpRequest() is a simply utility-method that checks whether HTTP request came up with X-Requested-With header with value XMLHttpRequest. So it's as reliable as X-Requested-With header is.
However, this is not really important. The important thing to notice is the fact that when the user clicks on the back button the browser does not send a new HTTP request to the server. It just restores the page from its internal memory/cache.
I understand that this is an old question, but the same issue just caught me so I figured I'd write up an answer anyway.
In most scenarios, you can invalidate the back button cache by attaching an onUnload handler to the window, like so:
window.addEventListener('unload',function(){});
or, if you prefer jQuery:
$(window).unload(function(){});
but, since your AJAX response is in JSON, that's obviously not possible since you can't include script fragments. In this case, I think the best idea is to set the cache-control: no-store header so the browser wont attempt to cache the result.
You can do that in the OP's case with Symfony2 using:
return new Response($res , 200 , array(
'Content-Type' => 'application/json',
'Cache-Control' => 'no-store',
));
or for more general PHP:
header('Cache-Control: no-store');
There's a caveat here in that it may degrade your performance quite a bit, depending on your app's structure, in which case your best bet would probably be to just use a different URL for your AJAX call. Sucks, I know.
You can find some documentation on the bfcache here, which may be more helpful in different cases.
The browser caches the response using just the url and the request method (GET, POST, etc) as the key.
If you want the browser to recognize additional variations, you can tell it to do so by setting the Vary header in your response. So in your case, you want to tell the browser that the response from the server will vary depending on whether or not the 'X-Requested-With' header was set in the request.
Here's how to do it:
$response = new Response();
$response->setVary("X-Requested-With"); // <=========== Set the Vary header
if($request->isXmlHttpRequest()) {
//...
}
return $response;
Note: that you want to set the Vary header on both versions of the response (this is why I've set it outside of the if statement).

Resources