Firstly, I can confirm that after a browser has finished uploading a file to the server, it does not close the connection. It is thus capable of receiving whatever error response e.g.
...
Downloads do appear to cause the browser to close the connection upon completion. This is just HTML, no JS involved. Further data from the server just disappears. And I am sending a fully formed web page.
I suspect some of the frustration in this thread originates from a differing mental model of HTTP. Please allow me to try to transfer some of my mental model to you.
HTTP is a
stateless protocol. It is communicated over a stateful TCP connection, which exposes a reliable duplex stream of bytes. An important note is that there is no such thing as a
HTTP connection, as data transfer with HTTP happens via a sequence of request-response pairs (in essence, though there exists HTTP/1.1 pipelining and all sorts of multiplexing in HTTP/2 and /3). Whether the underlying TCP connection is closed in between request-response pairs
depends, as I mentioned earlier. A client (e.g. a browser) makes a request for a resource (e.g. GET /path), the server responds with some data or a redirect, and that's it. Your browser only knows that a response is associated with its request because they happen sequentially in the TCP connection.
This is essentially what RFC-9110 "HTTP Semantics" says in the subsection
3.3. Connections, Clients and Servers and 3.4. Messages. As you're implementing a HTTP server from scratch, I would recommend glancing over the section 3. Terminology and Core Concepts. Even though it's a RFC, this one is quite human readable.
When you say that after "finishing uploading a file to a server, [a browser] does not close the connection" and that "downloads do appear to ... close the connection", my assumption is that you mean that you're trying to write a HTTP response with your microcontroller after handling a "download", and the server cannot send another document to the browser after already writing a "download".
Please open the image I've attached. It is a sequence diagram (created with PlantUML, if you're curious) of what happens when a user submits a small binary file to a server from the following page:
<!DOCTYPE html>
<html>
<body>
<form action="/" enctype="multipart/form-data" method="post">
<input type="file" name="file_input" />
<button type="submit">submit</button>
</form>
</body>
</html>
As you can see from the diagram, after the browser performs the form submission ("uploads a file"), it is expecting a request as I mentioned earlier. I think that this is related to your observation that "after ... uploading a file, [the browser] does not close
the connection", if by connection you mean "the request-response sequence". Indeed, it waits for a response from the server. In the diagram, the server responds to the "upload" by redirecting the browser to navigate to the address of the newly created file (302 Found) so that the user downloads said file.
The operation of "downloading" is in fact the browser making a request for a specific path, and the server sending a HTTP message with the contents of that file or whatever content it pleases as the message body. The meaning attached to the colloquial use of "downloading" also implies the familiar "Save as..." dialog, and the tab closing as the file begins to download to disk in the background. The only difference between this behavior, and your browser simply showing a web page (you navigating to a web address) is whether or not the server included
Content-Disposition: attachment in the headers of the response.
Turns out, there's actually nothing special about uploads or downloads - simply HTTP messages with headers and a body, which have semantics attached to them through standards (e.g. browser should use the POST method when submitting form data to the server and encode the fields like so and so, 204 No Content shouldn't have a body, etc.) and conventions (e.g. if I ask for /foo.exe, I'm expecting it to start a download for a file named foo.exe via Content-Disposition: attachment).
It now makes sense why "downloads cause the browser to close the connection upon completion", since the server has already written the response (the contents of the file), and it cannot write another response immediately after another one. From the perspective of the browser, it has completed what you've asked of it, and is ready to wait for the user's next move (what request shall we make next?).
LWIP must implement some sort of recovery, but it appears that the default setting is to disable timeouts. Could that possibly be right? Maybe it refers to different kinds of timeouts.
I'm not familiar with LWIP, but I would expect there to be
some timeouts at least. Perhaps your browser is just keeping the TCP socket open after you close the tab, if you have other tabs open? I would investigate this with Wireshark to check, or use your browser's developer tools (see below).
I used Wireshark a long time ago and really should get back to it. In fact just learning the browser debugging tools would be good. There is some way to see all data going back and forth but I have not been able to work it out.
Oh dear! So sorry to hear that you've been debugging blind! Of the 3 major browsers, I've found that Chrome (and other Chromium-based browsers like Edge) currently offers the best development tools currently. FF is pretty close as well.
Press F12 to open up the devtools drawer (you can move it around or pull it out into its own window from the three vertical dots menu on the right). There's
multiple ways of opening the devtools, but this one is simple.
- The Elements tab shows the currently rendered HTML (may differ from what the server sent - to see that, right click on the web page and choose "View source").
- The Console shows...console messages and warnings.
- The Network tab is what you're looking for here. It shows all requests with a waterfall display, allowing you to see request and response details, headers and body and all.
Enable "Preserve log" or otherwise it will clear out the list when your browser navigates. Click on a request to expand it. The details view has a Timing tab which breaks down what took how long during the request.
- The Performance tab and Memory tab is useful for profiling JavaScript in larger applications, of which the former allows you to throttle the CPU.
- The Application tab lets you see whatever information the current site has persisted, including cookies, LocalStorage, etc. ESC expands another utility drawer.
Chrome has comprehensive documentation for the devtools
here.
Another useful tool is just using
curl from the terminal. If you pass
-v, it'll enable
verbose mode and show you both the full request as well as the response. e.g.
curl -v https://google.comAs for making HTTP requests via a GUI, I've found
Postman to be quite handy. Just ignore their prompts to login or register.
Of the 3 browsers, only FF does the 500kbyte+ cache on uploads
Regarding this and your previous question about the ~500kB buffer thing, there are multiple buffers throughout the TCP/IP stack of both the sender and the receiver. I haven't noticed or paid attention to this behavior before, so I'm unable to comment there. I would start from the following:
- Open the FF devtools' Network tab and enable "Persist logs" from the cogwheel to the right
- See what the POST request looks like there, see its Timings tab in the details
- Take a look with tcpdump/Wireshark to see if there's anything obvious there
* does the TCP traffic look different between FF and Chrome?
- Investigate if this happens with other servers (try spinning one up locally and throttling the network speed?), or just your microcontroller server
* if just your stack, can you reproduce it with a different stack than LWIP? maybe LWIP buffers?
* can you reproduce it with a different client device on your server?
- What if you change your connection to, let's say, your phone's WiFi hotspot?
- If nothing else, maybe hop onto a Linux distro and use
strace to comb through the system calls Firefox makes during the uploading. It's possible to filter strace for specific syscalls, or just
grep it.
* Does it happen on a Linux machine as well?
Interesting that the keep alive causes the server to send 304 every 5 seconds. I could implement that, although my "server" is a primitive one which is mostly stateless and just responds to each client request immediately.
Sorry, I forgot to mention that the screenshot had both TCP and HTTP mixed (different levels of abstraction!). There was only one 304 response over HTTP, and the keep alive just instructed the client to keep the TCP connection open instead of closing it after receiving the response (and also communicated the server's intent that it is also intending on keeping the TCP connection open for at least 5 seconds). It
should be stateless and respond to each request immediately

. Unless we're talking about
WebSockets or
WebRTC or other technologies.