More on HTTP
— Seconds before disaster
Status Codes
Responses can have status codes attached to them. These indicate the status of a request.
Each status code is a number that indicates some information about the response.
For example, the status code 400 represents a Bad Request.
There are a lot of status codes and they're grouped in five classes:
- the status codes
100-199indicate informational responses (and you'll rarely see them) - the status codes
200-299indicate successful responses - the status codes
300-399indicate redirection responses - the status codes
400-499indicate client errors - the status codes
500-599indicate server errors
Quoting the famous HTTP status ranges in a nutshell, you could also rephrase this as:
100-199: hold on200-299: here you go300-399: go away400-499: you fucked up500-599: I fucked up
Let's have a quick look at the most important status codes.
The status code 200 (OK) is the standard response for a successful HTTP request.
The status code 201 (Created) is often used for requests where a resource has been successfully created.
The status code 400 (Bad Request) means that the server can't process the request due to a client error.
For example, a 400 should be returned if the client has provided the server with a malformed request.
The status code 401 (Unauthorized) usually means that the client couldn't be successfully authenticated.
The status code 403 (Forbidden) usually means that the client doesn't have the necessary rights to access the requested content.
Note that there is an important difference between 401 and 403.
The code 401 means that the client couldn't be authenticated at all, the code 403 means that the client could be authenticated, but doesn't have the correct access rights.
You've probably heard of the most famous status code 404 (Not Found).
As the name already says, this means that the server can't find the requested resource (usually because the resource doesn't exist).
The status code 405 (Method Not Allowed) means that the resource exists, but the client is trying to access it using the wrong method.
This occurs if, for example, a client is trying to send a GET request to a route that only supports POST requests.
The most important status codes for server errors are 500 (Internal Server Error) and 503 (Service Unavailable).
The status code 500 basically indicates a server crash, while 503 indicates that the server is overloaded.
Here is how you can manually return a status code in Express:
app.post('/', (req, res) => {
const data = req.body;
// If the request body is empty, we return status code 400 (Bad Request)
if (Object.keys(data).length === 0) {
res.status(400).send('Bad Request: No data provided');
} else {
res.status(201).send(`Received data: ${JSON.stringify(data)}`);
}
});
Try to curl the endpoint both with an empty and a non-empty object.
For example, you can curl the endpoint with an empty object like this:
curl -X POST -H "Content-Type: application/json" -d '{}' -w '\nStatus: %{http_code}' http://localhost:3000
The output would be:
Bad Request: No data provided
Status: 400
Let's also curl the endpoint with a non-empty object like this:
curl -X POST -H "Content-Type: application/json" -d '{"key": "value"}' -w '\nStatus: %{http_code}' http://localhost:3000
The output would be:
Received data: {"key":"value"}
Status: 201
Headers
Both requests and responses can have headers which provide important information that is not part of the main request or response content. For example, request headers might contain the version of the browser that is making the request, what languages are accepted, what encodings are accepted etc.
Here is how you can read request headers in Express:
app.get('/request-header-example', (req, res) => {
const customHeaderValue = req.get('Custom-Header');
if (customHeaderValue) {
const responseData = { 'Received-Custom-Header': customHeaderValue };
res.send(`Received data: ${JSON.stringify(responseData)}`);
} else {
res.status(400).send('No Custom-Header provided');
}
});
You can set a header in curl like this:
curl -H "Custom-Header: ExampleValue" http://localhost:3000/request-header-example
You will receive the following response:
Received data: {"Received-Custom-Header":"ExampleValue"}
Similarly, response headers might contain the type of the content, the date of the response etc.
Here is how you can set response headers in Express:
app.get('/response-header-example', (req, res) => {
const data = req.body;
// Add a custom header
res.set('X-Custom-Header', 'MyHeaderValue');
res.status(201).send(`Received data: ${JSON.stringify(data)}`);
});
You can see the response headers in curl by using the -i flag:
curl -i http://localhost:3000/response-header-example
This will output:
HTTP/1.1 201 Created
X-Powered-By: Express
X-Custom-Header: MyHeaderValue
Content-Type: text/html; charset=utf-8
Content-Length: 17
ETag: W/"11-10nwzUtYNUZcrMxeVU7fTdmCYaI"
Date: Thu, 14 Mar 2024 11:23:03 GMT
Connection: keep-alive
Keep-Alive: timeout=5
Received data: {}
Note that Express and other web frameworks already set quite a few response headers by default.
You can also see the response headers in the network tab of your browser if you click on an individual request.
Cookies
HTTP cookies are pieces of data that are created by the server, sent to the browser and then saved on the browser. Cookies can be used for things like authentication, personalization and tracking.
For example, the server might generate a cookie for a logged in user, send it to the browser and then the browser could send the cookie with each request (avoiding the need for the user to log in on every request).
Note that cookies are simply set as part of the headers.
For example, a request might contain a Cookie header containing the cookies it has currently stored.
A response might contain a Set-Cookie header which contains the cookies that the browser should set (or clear).
Let's have a look at a small example.
First, we'll need to install the cookie-parser package in addition to the express package:
pnpm install -g cookie-parser
Next, we will create an Express application with three routes.
The first route will be a /set-cookie route that will allow us to set a cookie.
The second route will be a /read-cookie route that will allow us to read the cookie.
Finally, we will add a third route /clear-cookie which will allows us to clear the cookie.
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
const PORT = 3000;
app.use(cookieParser());
// Route to set a cookie
app.get('/set-cookie', (req, res) => {
res.cookie('myCookie', 'test', { httpOnly: true });
res.send('Cookie has been set!');
});
// Route to read the cookie
app.get('/read-cookie', (req, res) => {
const myCookie = req.cookies.myCookie;
if (myCookie) {
res.send(`myCookie: ${myCookie}`);
} else {
res.status(404).send('No cookie found');
}
});
// Route to clear the cookie
app.get('/clear-cookie', (req, res) => {
res.clearCookie('myCookie');
res.send('Cookie has been cleared');
});
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}...`);
});
Let's launch the server, open a browser and go to http://localhost:3000/set-cookie.
You should see a message Cookie has been set! and a new key-value pair appearing in your "cookies" tab.
If you now go to http://localhost:3000/read-cookie, you will see myCookie: test on the page.
Finally, if you go to http://localhost:3000/clear-cookie, you will see that the cookie has been cleared.
Redirects
The 301 (Moved Permanently) status code is used for URL redirection.
This basically informs a client that the resource has been permanently moved.
A response with a 301 status code should contain the URL where the resource has been moved to.
The 302 (Found) status code is also used for URL redirection, but it indicates to the client that the resource has been temporarily moved.
Again, a response with a 302 status code should contain the URL where the resource has been moved to.
Let's have a look at an Express example:
const express = require('express');
const cookieParser = require('cookie-parser');
const app = express();
const PORT = 3000;
// 301 Redirect (Permanent)
app.get('/permanent-redirect', (req, res) => {
res.redirect(301, '/permanent-target');
});
// 302 Redirect (Temporary)
app.get('/temporary-redirect', (req, res) => {
res.redirect(302, '/temporary-target');
});
// Target Route for 301 Redirect
app.get('/permanent-target', (req, res) => {
res.send('This is the permanent target route');
});
// Target Route for 302 Redirect
app.get('/temporary-target', (req, res) => {
res.send('This is the temporary target route');
});
app.listen(PORT, () => {
console.log(`Server listening on port ${PORT}...`);
});
If you open http://localhost:3000/permanent-redirect in your browser, you will see that the address in the browser address bar suddenly changes to http://localhost:3000/permanent-target and you see the content 'This is the permanent target route'.
If you open the network tab, you will see that two requests are sent to accomplish this.
First, your browser sends a request to http://localhost:3000/permanent-redirect and receives a response with status code 301 and the response header Location set to /permanent-target.
Your browser now sends another request to the URL that is indicated in the Location header and receives the actual content.
You will see similar behaviour with the /temporary-redirect and /temporary-target routes.