Skip to content

Conversation

@guilleiguaran
Copy link
Contributor

@guilleiguaran guilleiguaran commented Jun 8, 2022

Description

Currently, Puma is not setting the Content-Length header when the Rack response body is an instance of Rack::BodyProxy due to the condition that is checking for an Array.

This PR takes an approach similar to Rack::ContentLength that checks if the body responds to to_ary instead of checking if the body is an Array.

Additionally, I added a sanity check in test_body_proxy just to confirm that Puma handles correctly Rack::BodyProxy responses.

Your checklist for this pull request

  • I have reviewed the guidelines for contributing to this repository.
  • I have added (or updated) appropriate tests if this PR fixes a bug or adds a feature.
  • My pull request is 100 lines added/removed or less so that it can be easily reviewed.
  • If this PR doesn't need tests (docs change), I added [ci skip] to the title of the PR.
  • If this closes any issues, I have added "Closes #issue" to the PR description or my commit messages.
  • I have updated the documentation accordingly.
  • All new and existing tests passed, including Rubocop.

Copy link
Member

@dentarg dentarg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@MSP-Greg
Copy link
Member

MSP-Greg commented Jun 9, 2022

@guilleiguaran Thank you for the PR.

Question, based of the code in Rack::ContentLength. If the body is an Array (responds to to_ary), should it be treated as a 'non-chunked' body?

IOW, if body is an Array, we compute the Content-Length. If the body isn't an Array or an IO/File (responds to read, etc), we consider it chunked, and handle it via each.

Thoughts? Making this change would require changing some tests, as many tests assume body arrays are chunked unless they contain one element. Using Array#to_enum probably fixes all of them...

@guilleiguaran guilleiguaran force-pushed the fix-rack-body-proxy-content-length branch from 25dacfb to 83c2ac2 Compare June 28, 2022 23:37
@guilleiguaran guilleiguaran force-pushed the fix-rack-body-proxy-content-length branch 5 times, most recently from 0ce9a53 to 1ab387b Compare August 27, 2022 07:35
@guilleiguaran guilleiguaran force-pushed the fix-rack-body-proxy-content-length branch from 100a8ee to 7701664 Compare August 27, 2022 09:48
@nateberkopec nateberkopec added waiting-for-changes Waiting on changes from the requestor bug labels Aug 27, 2022
@MSP-Greg MSP-Greg mentioned this pull request Aug 28, 2022
@guilleiguaran
Copy link
Contributor Author

guilleiguaran commented Aug 29, 2022

@MSP-Greg sorry for the delay, I've been working back and forth with multiple implementations of this, the current one handles array bodies with more than one element, Rack::BodyProxy, and also Rails' RackBody. Probably this needs to be updated to handle File-like objects as well (checking for to_path I guess?)

Rails' ActionDispatch::Response::RackBody (introduced 8 years ago in 4.2) responds to_ary just to conform with the implementation of Rack::ContentLength at the time in Rack 1.6, but it returns nil since Rack::ContentLength used #each without calling to to_ary. Rack 2.2 stopped checking for to_ary and Rack 3.0 (master) once again checks for to_ary and actually calls it.

There have been many discussions around how to decide if a body should be considered to have a knowable length or not in both Rack and Rails, see:

rails/rails#16793
rack/rack#734
rack/rack@9495207
rack/rack#1748
rack/rack#1509
rails/rails#44953

str_headers(env, status, headers, res_info, lines, requests, client)

if !res_info[:content_length] && !res_info[:transfer_encoding] && status != 204
if res_body.respond_to?(:to_ary)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: this condition can be merged with the previous one

if res_body.respond_to?(:to_ary)
if array_body = res_body.to_ary
res_info[:content_length] = array_body.map(&:bytesize).inject(0, :+)
elsif res_body.respond_to?(:each)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This to_ary exists but returns nil is for handling objects like Rails' RackBody

@MSP-Greg
Copy link
Member

MSP-Greg commented Aug 29, 2022

@guilleiguaran

sorry for the delay

No problem.

Thanks for all the links. The 'returns nil' code is interesting, to put it nicely.

I wonder why an app would not set Content-Length if the body had a knowable length. That aside, I wonder if something like the following would get around all the issues? Note, I need a refresh based on your above comments...

if (app_body.respond_to?(:to_ary) && (body = app_body.to_ary)) ||
    (app_body.is_a?(Array) && app_body.all? { |el| el.is_a? String } && body = app_body)
  # compute length from body, use body to write response
end

app_body is the body returned by the app, currently called res_body.

@MSP-Greg
Copy link
Member

@guilleiguaran

Dumb Rails question:

Would a normal Rails app ever return a body where body.is_a? Array is true, or, better yet, body.class == Array is true?

@guilleiguaran
Copy link
Contributor Author

@MSP-Greg no for anything handled through ActionDispatch (e.g Rails controllers), those are always wrapped in ActionDispatch::Response::RackBody

For the next version of Rails, I'll work in proper support for Rack 3.0 and Rack::ContentLength (supporting to_ary for real, not returning nil)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug waiting-for-changes Waiting on changes from the requestor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants