sammhicks / picoserve

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

Request body as `io::Read` #21

Closed Ddystopia closed 9 months ago

Ddystopia commented 9 months ago

Hello,

we use picoserve for updating firmware and configurations on embedded devices. Our updates are large, and storing the entire body in RAM is resource-intensive.

We suggest enhancing request.body() to implement io::Read instead of holding the entire payload in memory. Our devices support async writes, and streaming the body directly would greatly reduce RAM usage, benefiting not only us but anyone with similar embedded device constraints.

This change would streamline operations for large payloads, making picoserve more efficient for embedded systems.

Ddystopia commented 9 months ago

I guess it contradicts #19

Ddystopia commented 9 months ago

I made some version of it, like that:

Right now I modified FromRequest trait to accept a reader too, but I guess it doesn't make a lot of sense. I guess better solution would be to let it as it is, so implementors would now not have access to the body and add new trait that would get a reader. Than in that huge triangular macro it can impl for an additional type of handler, with that trait. So 0 or 1 types that implement that new trait that has access to the body reader.

Also I'm not sure how to deal with content length. I guess a reader handed to that trait implementation could limit read bytes to content length and also count how many bytes were read, so that if implemetor did not consume body, some other code would consume it and discard.

Ddystopia commented 9 months ago

What is your opinion about it? Would you consider merging if I made pull request?

sammhicks commented 9 months ago

I'm definitely interested. I'm currently also working on what is hopefully a solution, but a different solution as well will highlight stuff that I've missed or not thought of, and the final solution can be the best of both approaches. Thanks for the help :)

Ddystopia commented 9 months ago

Okay. Right now it is not in a beautiful state with generics splattered all over the code (I already see that in .*write.* traits/methods it could be rolled back), but it was without any design thought, just to get it working. Also I made request::Reader to read only by 1 byte so that it does not consume body, but BodyReader could have a slice to already read body and return it first before using underlying reader.

sammhicks commented 9 months ago

Thanks for the code share, it's been a great help! Your design of having the routing methods be generic over the connection read type is much neater than the spaghetti that I ended up with which included the read type being part of the trait themselves. I also liked the mechanism of having a body reader that you finalize and then pass to the ResponseWriter :)

I've put the initial design in the branch request-body-read, and have put an example of streaming data called huge_requests. Roughly speaking, it has the following new features:

TODO:

sammhicks commented 9 months ago

Update: I've found several bugs with the new implementation, currently investigating and fixing...

Ddystopia commented 9 months ago

Thank you!