Status 1xx are obsolete?
Not, there is a translation error in the excerpt you quoted from Wikipedia.
The original passage is:
Since HTTP/1.0 Did not define any 1xx
status codes, Servers MUST NOT send a 1xx Response to an HTTP/1.0
client except under experimental conditions.
Where the term "Since" was translated as "from the", but in this context it would be better translated as "since", "as the" or something of the kind, that is, subordinative locution, of that which expresses the cause.
A better translation would be:
Since HTTP/1.0 does not define 1xx status codes, servers MUST NOT send a 1xx response to an HTTP/1.0 client unless under experimental conditions.
That is, this does not mean that the 1xx status code is obsolete, but on the contrary, that it was started to use from HTTP/1.1 onwards.
What is the purpose of these responses?
Allow the client to send headers and body of the request separately. This is because, in some situations, the server will be able to determine whether to accept a request or not only with the header information. In cases where the body of the request is too large there would be an unnecessary loss of resource when sending it to the server and having the request denied. Two main features that would be directly impacted by this would be the response time, which by moving more information is expected to take longer to deliver the message, and the bandwidth consumption of both the client and the server, by transmitting data that will not be used.
A fairly common situation that would take advantage of these responses are file uploads to the server. It is common for the server or application to have validations regarding the type (mimetype) of the file and its size. Two information that would already be present in the request header, so the server could refuse it without actually receiving the file.
Imagine the two applications (browser and server) chatting:
- Browser: Good morning, server, I would like to send you a file;
- Browser: It is a video file, MKV format;
- Browser: It is 1.21 GB in size;
- Browser: Please confirm if I can send it;
- Server: Hello, browser, do not send, I only accept PDF files with less than 5 MB;
In this case, the browser didn’t even have to send 1.21 GB of data for no reason (imagine sending it by 4G), only to know that the server would refuse the request. In a valid case something quite similar would happen:
- Browser: Good morning, server, I would like to send you a file;
- Browser: It is a PDF file format;
- Browser: It is 1.21 MB in size;
- Browser: Please confirm if I can send it;
- Server: Hello, browser, everything looks ok, continue;
- Browser: Thank you, here is the file...
That is, if with the information present in the headers is not enough to refuse the request, will be returned to the customer to continue and send the body of the same. Note that this is not a guarantee of success, as the data sent by the body may be corrupted, from another file, etc.
Shattering expectations
If you notice the above dialogs in both cases the browser has asked for confirmation from the server to continue with the request. By default HTTP does not work like this, so you need to explicitly state when the client will wait for confirmation. This generates an expectation in the client, which is sent to the server through the header Expect
, informing what is the expectation.
POST /upload HTTP/1.1
Content-Type: application/pdf
Content-Length: 1210000
Expect: 100-Continue
...
When informing 100-Continue
in the header Expect
the client is informing the server that it will wait for its confirmation to continue the process. If the server authorizes the request, it must respond with 100 Continue
, but if you refuse, you will answer with 417 Expectation Failed
.
Considerations on servers and proxies
When a server receives a request:
Upon receiving a request that includes the header Expect: 100-Continue
, the server MUST respond with the status 100 Continue
and continue reading the data input stream or sending a final response, denying the request. The server MUST NOT wait for the body of a request BEFORE sending the response 100 Continue
. By submitting a final response rejecting the request, the connection to the client CAN be closed or CAN continue open, discarding any data arriving from the rejected request.
The server MUST NOT send the answer 100 Continue
if the client request does not have the header Expect: 100-Continue
and MUST NOT send it also if the client is using HTTP/1.0, as already mentioned. For RFC 2068 compatibility reasons, the server CAN respond with 100 Continue
a request that does not have the header Expect
of methods PUT
or POST
, HTTP/1.1 version only.
The server CAN omit the answer 100 Continue
when you have already received the entire body of the request previously.
The server MUST send a final response after receiving and processing the body of the request.
When the request is handled by a proxy:
If a proxy receives a request that includes the header Expect
with the value 100-Continue
, and the proxy knows that the next entity works with HTTP/1.1 or higher, or does not know the version of HTTP that this entity accepts, the proxy MUST forward the message including the header Expect
.
If the proxy, under the same conditions as the previous one, knows that the next entity only operates with HTTP/1.0, the proxy SHOULD NOT forward the message, instead it should already respond to the client with 417 Expectation Failed
.
Proxy MUST keep cached the version the next entity uses to be possible for the two previous checks.
The proxy MUST NOT forward any response to the customer 100 Continue
that eventually received if the client’s original request was by HTTP/1.0.
And how the server handles it?
I should start by saying that it depends on the server. I couldn’t find official information on how Apache and Nginx (I’ve never worked with anyone other than these) handle the header Expect
, if anyone knows any reliable documentation please share in the comments or in another reply.
Life cycle of a request without Expect
First, we will analyze the life cycle of a "normal request":
Cliente Servidor Aplicação
->█ | |
█- Cabeçalhos da Requisição -------->█ |
█ █ |
█- Corpo da Requisição ------------->█ |
| █ |
| █- Requisição Completa --------------->█
| | █-[Processamento]
| █<----------------- Resposta Completa -█
| █ |
█<---------- Cabeçalhos da Resposta -█ |
█ █ |
█<--------------- Corpo da Resposta -█ |
<-█ | |
In this case, we have:
- Client sends to server the request headers;
- Client sends to server the body of the request;
- The server runs the application by providing an API to access the request information;
- The application is executed by generating a response that is sent to the server;
- The server sends the response headers to the client;
- The server sends the response body to the client;
As commented, in this configuration the client must send the complete request to the server, which will analyze and serve the application, the latter being responsible for defining whether the request will be processed or not. In many cases this decision can be made only based on the request headers and this is precisely what the header Expect
search around.
Life cycle of a request with Expect
When the request has Expect
is generated the possibility of action by the server whether or not to refuse the request only based on the headers.
Cliente Servidor Aplicação
->█ | |
█- Cabeçalhos da Requisição -------->█ |
| █-[Validação] |
█<------------- Resposta 100 ou 417 -█ |
█ | |
<-█-[Erro se 417] | |
█ | |
█- Corpo da Requisição ------------->█ |
| █ |
| █- Requisição Completa --------------->█
| | █-[Processamento]
| █<----------------- Resposta Completa -█
| █ |
█<---------- Cabeçalhos da Resposta -█ |
█ █ |
█<--------------- Corpo da Resposta -█ |
<-█ | |
In this case, we have:
- Client sends to server the request headers;
- The server performs a previous validation of the headers and defines whether to accept (100) or fail (417) the request;
- When server returns 417 the cycle ends by returning the error to the client;
- When server returns 100 the client continues sending to the server the body of the request;
- The server runs the application by providing an API to access the request information;
- The application is executed by generating a response that is sent to the server;
- The server sends the response headers to the client;
- The server sends the response body to the client;
Steps 2, 3 and 4 are what differ this configuration from the previous one precisely at the moment when the server is given the possibility to accept or refuse the request only based on the headers. Apart from that, we can notice some peculiarities that are of utmost importance when implementing:
The client, when sending a request having the header Expect
, must be able to receive an intermediate response (100 or 417) from the server before effectively sending the body of the request;
The client must define how it will handle the occasions when the server does not send the intermediate response (100 or 417); in such cases the client can (1) assume that the request was failed and abort or (2) send the body of the request anyway and check what will be the final response of the server. Option (2) is usually more common because it is naturally compatible with servers that do not handle the header Expect
. In this case, the request life cycle is identical to the request cycle that does not have Expect
;
The server must be able to receive the body of the request after sending the intermediate response to the client (100 or 417);
The server must be able to handle situations where the client will not send the body of the request after the intermediate response, possibly aborting the connection (timeout);
The server must be able to handle clients sending the request body even before receiving the intermediate response - in cases the client does not have header support Expect
, the server must omit the intermediate response (100 or 417) and process the request normally; the request lifecycle is identical to that of the request that does not have Expect
;
In this configuration you are, in a way, limited to do the validation exclusively on the server, that is, the validation that will define whether to accept a request or not be made based on the server settings or environment variables. There is also the possibility to transfer this validation to the application (remember that here we are talking about implementing, from scratch, a web server and we are not stuck to the settings of commercial servers).
Life cycle of a request with Expect
validated by the application
The life cycle of a request that would be previously validated by the application would be:
Cliente Servidor Aplicação
->█ | |
█- Cabeçalhos da Requisição -------->█ |
| █- Cabeçalhos da Requisição ---------->█
| | █-[Validação]
| █<--------------- Resposta 100 ou 417 -█
█<------------- Resposta 100 ou 417 -█ |
█ | |
<-█-[Erro se 417] | |
█ | |
█- Corpo da Requisição ------------->█ |
| █ |
| █- Requisição Completa --------------->█
| | █-[Processamento]
| █<----------------- Resposta Completa -█
| █ |
█<---------- Cabeçalhos da Resposta -█ |
█ █ |
█<--------------- Corpo da Resposta -█ |
<-█ | |
In this case we have basically the same life cycle as the previous one, where the server decided whether or not to accept the request, but now this responsibility is passed on to the application itself leaving the architecture considerably more versatile than the other settings. However, to be possible the application itself must support running only with the request headers, which makes the application not compatible with servers without support for the Expect
, where the requisition would be delivered completely at once.
Implementation (In Python)
For all this to be possible, we will have to work at the level of socket, where we have control of the communication between the parties - both on the client and on the server. This way, a request made through a browser is out of our control and if it will use Expect
or it will not depend exclusively on the development team when dealing with the connection between browser/server. A well-known customer who has support for Expect
is Curl when set to property --expect100-timeout <seconds>
(in fact Curl uses the Expect
by default when it is a PUT request or a POST request over 1024 bytes in size).
For demonstration purposes, a fully functional Python server will be implemented using the library asyncio
due to the ease of implementation. The server will be considered to respond with an echo of the request body as the response body when the request body does not exceed 42 bytes.
Example request sending less than 42 bytes
> GET / HTTP/1.1
> Host: localhost
> Content-Type: text/plain
> Content-Length: 35
> Expect: 100-continue
< HTTP/1.1 100 Continue
> Um texto com menos de 42 caracteres
< HTTP/1.1 200 Ok
< Content-Type: text/plain
< Content-Length: 35
< Um texto com menos de 42 caracteres
Example request sending more than 42 bytes
> GET / HTTP/1.1
> Host: localhost
> Content-Type: text/plain
> Content-Length: 45
> Expect: 100-continue
< HTTP/1.1 417 Expectation Failed
< Content-Length: 37
< Nao e permitido mais de 42 caracteres
X Ja este texto tem muito mais de 42 caracteres
Code time, baby!
Code written in Python 3.7, would work in 3.6 with minor adaptations.
import asyncio
from typing import Coroutine
async def echo_server(
reader: asyncio.StreamReader,
writer: asyncio.StreamWriter
) -> Coroutine:
# Read the status line of the request
status_line: bytes = await reader.readline()
method, resource, version = status_line.decode('ascii').split()
# Read the request headers
headers: list = []
has_expect_header: bool = False
content_length: int = 0
while True:
header_line: bytes = await reader.readline()
# End of headers
if not header_line.strip():
break
header_name: str
header_value: str
header_name, header_value = header_line.decode('ascii').split(':', 1)
headers.append((header_name.strip(), header_value.strip()))
if header_name.strip().lower() == 'expect':
has_expect_header = True
elif header_name.strip().lower() == 'content-length':
content_length = int(header_value)
# Has the Expect header?
if has_expect_header:
# Validation
if content_length > 42:
writer.write(b'HTTP/1.1 417 Expectation Failed\n')
writer.write(b'Content-Length: 37\n\n')
writer.write(b'Nao e permitido mais de 42 caracteres')
await writer.drain()
writer.close()
return
writer.write(b'HTTP/1.1 100 Continue\n\n')
# Read the request body
body: bytes = await reader.read(content_length)
# Send the echo response
writer.write(b'HTTP/1.1 200 Ok\n')
writer.write(f'Content-Length: {content_length}\n'.encode('ascii'))
writer.write(b'Content-Type: text/plain\n\n')
writer.write(body)
await writer.drain()
writer.close()
async def main(host, port):
server = await asyncio.start_server(echo_server, host, port)
await server.serve_forever()
asyncio.run(main('127.0.0.1', 5000))
Code starts a server at 127.0.0.1:5000 receiving HTTP requests. If you have the header Expect
the server will validate Content-Length
; if less than or equal to 42 (limit stipulated for the example), the 100 Continue reply will be sent allowing the customer to send the body of the request; if Content-Length
surpass the value 42 the answer 417 Failed Expectation is returned by terminating communication.
Starting the server
When executing the script, we can use Curl as client for validation.
$ python main.py
Requisition with Expect
valid:
$ curl -v -H "Expect: 100-continue" -d "Um texto com menos de 42 caracteres" 127.0.0.1:5000
* Rebuilt URL to: 127.0.0.1:5000/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 5000 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:5000
> User-Agent: curl/7.58.0
> Accept: */*
> Expect: 100-continue
> Content-Length: 35
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 100 Continue
* We are completely uploaded and fine
< HTTP/1.1 200 Ok
< Content-Length: 35
< Content-Type: text/plain
<
* Connection #0 to host 127.0.0.1 left intact
Um texto com menos de 42 caracteres
See that Curl sends the headers, the server responds with 100 Continue, the Curl displays the log message We are completely uploaded and fine, sends the body of the request and finally receives the final response, 200, from the server with the echo of the request.
Image 1: Valid request made manually from telnet 127.0.0.1 5000
sending the header Expect
to a server configured to receive a maximum of 5 bytes
Requisition with Expect
invalid:
$ curl -v -H "Expect: 100-continue" -d "Ja este texto tem muito mais de 42 caracteres" 127.0.0.1:5000
* Rebuilt URL to: 127.0.0.1:5000/
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 5000 (#0)
> POST / HTTP/1.1
> Host: 127.0.0.1:5000
> User-Agent: curl/7.58.0
> Accept: */*
> Expect: 100-continue
> Content-Length: 45
> Content-Type: application/x-www-form-urlencoded
>
< HTTP/1.1 417 Expectation Failed
< Content-Length: 37
* HTTP error before end of send, stop sending
<
* Closing connection 0
Nao e permitido mais de 42 caracteres
See that Curl sends headers, the server responds with 417 Expectation Failed, as we are sending 45 bytes to a server that supports at most 42, Curl displays the log message HTTP error before end of send, stop sending showing that an error occurred before closing the upload and therefore terminates the communication, not sending the body of the request to the server.
Image 2: Invalid request made manually from telnet 127.0.0.1 5000
sending the header Expect
to a server configured to receive a maximum of 5 bytes
Readings and references
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/100
- https://tools.ietf.org/html/rfc7231#Section-6.2.1
- https://www.w3.org/Protocols/rfc2616/rfc2616-sec8.html#sec8.2.3
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/417
- https://tools.ietf.org/html/rfc7231#Section-5.1.1
I guess I owe the rest of the hahaha Oops answer
– Woss
@Andersoncarloswoss no problem, your answer, as it is, already deserves a reward
– Costamilam