sammhicks / picoserve

An async no_std HTTP server suitable for bare-metal environments, heavily inspired by axum
MIT License
177 stars 20 forks source link

Support multipart/x-mixed-replace responses #43

Open Dominaezzz opened 1 week ago

Dominaezzz commented 1 week ago

This is needed to implement an MJPEG stream. A typical multipart response looks something like this.

HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=myboundary

--myboundary
Content-Type: image/jpeg
Content-Length: 12345

[image 1 encoded jpeg data]

--myboundary
Content-Type: image/jpeg
Content-Length: 45678

[image 2 encoded jpeg data]

At the moment I can't implement this outside the library because the Content trait mandates that I provide a content_length() and I don't think there's any public way to create a (custom) Response without a Content-Length header. So I guess this has to implemented inside the library similar to SSE and WebSockets? I'm imagining something like the EventStream struct, A MixedReplace struct that takes two parameters, a boundary string and a ContentSource trait. The ContentSource trait just allows the user to write new Contents. (I'm not 100% sure if Content is the right trait for this, was just my first thought)

Side note: Firefox seems to not mind a Content-Length atm, so specifying a value like 1000000 allows me to workaround this issue while developing and I'm able to stream small JPEGs with this.

(This library is fantastic btw! Thanks for building it and sharing it!)

sammhicks commented 3 days ago

multipart/x-mixed-replace seems very different to regular HTTP requests and responses, even things like SSE and websockets, so I don't think that I want an implementation in picoserve itself.

Originally I decided to be relatively restrictive with what sorts of responses could be produced, hence Response having private fields, and Response::new requiring a body of Content, which requires a known length and MIME type, as opposed to the more general Body, with no such restrictions. This was both for developer ergonomics, in that it avoided footguns to do with forgetting to send the relevant headers, but also helped prevent me coding myself into a corner and having major breaking changes.

However, now that picoserve has been out for a while with a relatively stable structure, and that I've implemented support for automatically handing the "Connection" header and Keep-Alive, it seems a good time to provide functionality to create unusual responses that don't follow the standard pattern.

Thus, I introduce the CustomBody trait and CustomResponse struct, similar to SSE, WebSockets, and Chunked Transfer Encoding. You should be able to implement multipart/x-mixed-replace on top of it.

It's currently in the development branch, let me know if it works for you!

Dominaezzz commented 2 days ago

Yes CustomBody and CustomResponse let me implement multipart/x-mixed-replace. I just tried it and it works like a charm.

Now that Connection is no longer a parameter of write_content, how can I tell when the connection is closed? I used to have some cleanup logic with wait_for_disconnect.

sammhicks commented 1 day ago

Is your cleanup login sync or async? If sync, can your CustomBody implement Drop?

Dominaezzz commented 1 day ago

Ah sorry, I misrepresented my problem. My clean up logic is sync but even if I implement Drop, when I cancel the stream in the browser, the server hangs and hits a write timeout. During this hang time I can't handle more requests.

Ideally I'd like the write to be cancelled immediately, which I was previously doing myself by calling connection.wait_for_disconnect().