I'm developing a webgame. As part of the game, you start out with a limited set of features, and you unlock more of them as you play.
For instance, you unlock /fields
as part of step 3 in the tutorial. But what if you just navigate to /fields
in the address bar?
I'm trying to work out what would be the best status code to respond with.
403 seems ideal since the user is forbidden from accessing the page until they unlock it.
404 also makes sense since the page technically "doesn't exist" until it is unlocked and also prevents users from being able to tell the difference between a page that doesn't exist and one that they just haven't unlocked yet.
But in both cases I've had some users report issues with the browser cacheing the 403/404 result and not letting them access the page even after unlocking it unless they purge the cache entirely.
I'm wondering if I should keep using 403 or 404, or should I use an unused 4XX code such as 442 with a custom statusText, or even jokingly send HTTP/1.1 418 I'm A Teapot
in response to a user poking around where they shouldn't be.
I need a good, solid reason why one option should be used over the others.
tl;dr 409 Conflict
would be an idea, but perhaps you have problems with caching. In this case a cache-buster to force a reload will work.
Long explanation
Perhaps a 409 Conflict
status code would make sense:
10.4.10 409 Conflict
The request could not be completed due to a conflict with the current state of the resource. This code is only allowed in situations where it is expected that the user might be able to resolve the conflict and resubmit the request. The response body SHOULD include enough information for the user to recognize the source of the conflict. Ideally, the response entity would include enough information for the user or user agent to fix the problem; however, that might not be possible and is not required.
Conflicts are most likely to occur in response to a PUT request. For example, if versioning were being used and the entity being PUT included changes to a resource which conflict with those made by an earlier (third-party) request, the server might use the 409 response to indicate that it can't complete the request. In this case, the response entity would likely contain a list of the differences between the two versions in a format defined by the response Content-Type.
It would make sense, because the resource is only available after the user did the tutorial. Before that the resource is in an «invalid» state. And the user is able to resolve this conflict by completing the tutorial.
Later I investigated the case a little more and I discovered that the devil is in the detail. Let's read the specification for 403 Forbidden
and 404 Not Found
.
10.4.4 403 Forbidden
The server understood the request, but is refusing to fulfill it. Authorization will not help and the request SHOULD NOT be repeated. If the request method was not HEAD and the server wishes to make public why the request has not been fulfilled, it SHOULD describe the reason for the refusal in the entity. This status code is commonly used when the server does not wish to reveal exactly why the request has been refused, or when no other response is applicable.
Important is the specification that «the request SHOULD NOT be repeated». A browser which never re-requests a 403 page might do the right thing. However, let's continue with 404:
10.4.5 404 Not Found
The server has not found anything matching the Request-URI. No indication is given of whether the condition is temporary or permanent.
[omitted]
Now we have a problem! Why would your 404 pages be cached if the specification allows them to be temporary?
Perhaps in your setup you have caching configured not correctly for your 403 and 404 pages. If this is so, please consult this answer on StackOverflow. It gives a detailed answer about caching 4xx pages.
If you don't want to mess with caching headers, use a so-called cache-buster and pass the system time like this (assuming PHP as your web language):
<a href="/fields?<?php echo time(); ?>">
This produces URLs like /fields?1361948122
, increasing every second. It's a variant of the solution proposed by Markus A.
I assume the querystring 1361948122
is ignored by your resource. If it is not, pass the cache-buster in a querystring parameter instead, for example t=1361948122
and make sure that the parameter t
is not evaluated by your resource.