Compare commits

...

15 Commits

Author SHA1 Message Date
MSP-Greg f323d129bc
[CI] - test_integration_single.rb - fix curl msg regex (#3012) 2022-11-04 15:32:48 -05:00
Juanito Fatas 70600bf261
Handle waking up a closed selector in Reactor#add (#3005)
Co-Authored-By: MSP-Greg <MSP-Greg@users.noreply.github.com>

Co-authored-by: MSP-Greg <MSP-Greg@users.noreply.github.com>
2022-11-04 13:38:01 +09:00
MSP-Greg 3d33475dd4
Correctly close app body for all code paths (#3002)
* request.rb - Close app body always

* test_rack_server.rb - add test_hijack_body_close

* puma_server.rb - refactor, takes too long

* Add `test_lowlevel_error_body_close` to verify body close call when lowlevel_error

* [CI] Fix two intermittent failures?
2022-11-03 15:46:43 +09:00
James Prior 9e131b6098
Include `ca` option for SSL bind and mTLS example (#3009)
In digging through the SSL options in code I noticed that it supports more use cases than documented, this doesn't cover all of them but expands a bit.
2022-11-03 15:41:12 +09:00
Memuna Haruna d27820ff3f
Skip duplicate Github Actions runs (#2929) (#3008)
* Skip duplicate Github Actions runs (#2929)

Closes #2929

Skip conditions have been added at the step level for matrix jobs because of:
https://github.com/marketplace/actions/skip-duplicate-actions#how-to-use-skip-check-with-required-matrix-jobs

* Update the skip_duplicate_workflow_runs workflow

Remove the `cancel_others` setting since cancelling workflow runs from outdated commits requires the `actions: write` permission.
2022-11-02 13:37:30 +09:00
Nate Berkopec 1a3a46aba2
Contributing update [ci skip]
Quick clarification on issue tracker
2022-10-28 14:09:43 +09:00
MSP-Greg 7dba0746d7
[CI] Fix TruffleRuby TestPumaServer#test_lowlevel_error_message_without_backtrace (#3001) 2022-10-25 21:00:16 -05:00
Patrik Ragnarsson 03ed6c8e78
TestBundlePruner: do not hard code directory name (#2995)
These two test would fail when running tests from a directory not named
"puma", e.g. running from docker/podman like this:

    podman run -it --rm -v $(pwd):/app -w /app ruby:3.1.2 bash
2022-10-22 15:06:05 +02:00
Juanito Fatas de5bb82227
Fix path could be nil in test_puma_server tests (#2981)
* Fix path could be nil in test_puma_server tests

When I run the test locally with
ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [arm64-darwin21]
I got two failing tests[1] which complain path is nil.

Rearrange how we use tempfile (and use syswrite) in tests.
Extract into a tempfile_create method in test/helper.rb.

[1]

  1) Error:
TestPumaServer#test_file_body:
TypeError: no implicit conversion of nil into String
    test/test_puma_server.rb:158:in `exist?'
    test/test_puma_server.rb:158:in `ensure in test_file_body'
    test/test_puma_server.rb:158:in `test_file_body'
    /Users/hhh/dev/puma/test/helper.rb:89:in `block (4 levels) in run'
    /Users/hhh/.rubies/ruby-3.1.0/lib/ruby/3.1.0/timeout.rb:107:in `block in timeout'
    /Users/hhh/.rubies/ruby-3.1.0/lib/ruby/3.1.0/timeout.rb:117:in `timeout'
    /Users/hhh/dev/puma/test/helper.rb:87:in `block (3 levels) in run'

  2) Error:
TestPumaServer#test_file_to_path:
TypeError: no implicit conversion of nil into String
    test/test_puma_server.rb:177:in `exist?'
    test/test_puma_server.rb:177:in `ensure in test_file_to_path'
    test/test_puma_server.rb:177:in `test_file_to_path'
    /Users/hhh/dev/puma/test/helper.rb:89:in `block (4 levels) in run'
    /Users/hhh/.rubies/ruby-3.1.0/lib/ruby/3.1.0/timeout.rb:107:in `block in timeout'
    /Users/hhh/.rubies/ruby-3.1.0/lib/ruby/3.1.0/timeout.rb:117:in `timeout'
    /Users/hhh/dev/puma/test/helper.rb:87:in `block (3 levels) in run'

Co-Authored-By: MSP-Greg <MSP-Greg@users.noreply.github.com>

* Ensure we always close tempfile

seems like Windows will be happy if we do this

* Large body adjustments

Co-authored-by: MSP-Greg <MSP-Greg@users.noreply.github.com>
Co-authored-by: MSP-Greg <Greg.mpls@gmail.com>
2022-10-22 15:44:01 +09:00
MSP-Greg 1520f6bfb0
[CI] - remove macOS 10.15 jobs, unsupported as of 01-Dec-2022 (#2994) 2022-10-21 15:55:08 -05:00
Andrey Novikov c975c096f6
Remove outdated entry from 6.0-Upgrade.md about server initialization signature (#2992)
[ci skip]
2022-10-19 17:59:09 +02:00
Nate Berkopec a9f659ffee
Re-add new contributor calls to CONTRIBUTING.md [ci skip] 2022-10-19 15:47:39 +09:00
Nate Berkopec 13ee96d138
Add note about making Github release [ci skip] 2022-10-19 15:43:57 +09:00
Elia Schito 325ad31ac6
Mark #2798 as a breaking change in the changelog (#2991)
[ci skip]
2022-10-15 23:12:19 +02:00
Patrik Ragnarsson cfb0477350
Document that `DefaultRackup` et al. was removed (#2990)
[ci skip]
2022-10-14 20:33:27 +02:00
16 changed files with 275 additions and 128 deletions

View File

@ -1,21 +1,18 @@
name: Rack_v2
on:
push:
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
on: [push, pull_request, workflow_dispatch]
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
skip_duplicate_runs:
uses: ./.github/workflows/skip_duplicate_workflow_runs.yaml
rack_v2:
name: >-
Rack_v2: ${{ matrix.os }} ${{ matrix.ruby }}
needs: skip_duplicate_runs
env:
CI: true
TESTOPTS: -v
@ -35,9 +32,11 @@ jobs:
steps:
- name: repo checkout
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
uses: actions/checkout@v3
- name: load ruby
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
uses: ruby/setup-ruby-pkgs@v1
with:
ruby-version: ${{ matrix.ruby }}
@ -50,18 +49,23 @@ jobs:
# fixes 'has a bug that prevents `required_ruby_version`'
- name: update rubygems for Ruby 2.4 - 2.5
if: contains('2.4 2.5', matrix.ruby)
if: |
contains('2.4 2.5', matrix.ruby) &&
(needs.skip_duplicate_runs.outputs.should_skip != 'true')
run: gem update --system 3.3.14 --no-document
continue-on-error: true
timeout-minutes: 5
- name: set WERRORFLAG
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
shell: bash
run: echo 'PUMA_MAKE_WARNINGS_INTO_ERRORS=true' >> $GITHUB_ENV
- name: compile
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
run: bundle exec rake compile
- name: test
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
timeout-minutes: 10
run: bundle exec rake test:all

View File

@ -1,23 +1,20 @@
name: ragel
on:
push:
paths:
- 'ext/**'
- '.github/workflows/ragel.yml'
pull_request:
paths:
- 'ext/**'
- '.github/workflows/ragel.yml'
workflow_dispatch:
on: [push, pull_request, workflow_dispatch]
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
skip_duplicate_runs:
uses: ./.github/workflows/skip_duplicate_workflow_runs.yaml
with:
paths: '["ext/**", ".github/workflows/ragel.yml"]'
ragel:
name: >-
ragel ${{ matrix.os }} ${{ matrix.ruby }}
needs: skip_duplicate_runs
env:
PUMA_NO_RUBOCOP: true
PUMA_TEST_DEBUG: true
@ -37,15 +34,19 @@ jobs:
steps:
# windows git will convert \n to \r\n
- name: git config
if: startsWith(matrix.os, 'windows')
if: |
startsWith(matrix.os, 'windows') &&
(needs.skip_duplicate_runs.outputs.should_skip != 'true')
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- name: repo checkout
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
uses: actions/checkout@v3
- name: load ruby
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
uses: ruby/setup-ruby-pkgs@v1
with:
ruby-version: ${{ matrix.ruby }}
@ -55,6 +56,7 @@ jobs:
timeout-minutes: 10
- name: check ragel generation
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
shell: pwsh
run: |
ragel --version

View File

@ -0,0 +1,32 @@
name: Skip Duplicate Workflow Runs
on:
workflow_call:
inputs:
paths:
description: 'A JSON-array with path patterns'
default: '[]'
required: false
type: string
outputs:
should_skip:
description: "The output from the skip_duplicate_runs job"
value: ${{ jobs.skip_duplicate_runs.outputs.should_skip }}
permissions:
contents: read
jobs:
skip_duplicate_runs:
name: 'Skip Duplicate Runs'
runs-on: ubuntu-latest
outputs:
should_skip: ${{ steps.skip_check.outputs.should_skip }}
steps:
- id: skip_check
uses: fkirc/skip-duplicate-actions@v5.2.0
with:
paths_ignore: '["**.md"]'
paths: ${{ inputs.paths }}
concurrent_skipping: 'same_content_newer' # skip newer runs with same content
skip_after_successful_duplicate: 'true'

View File

@ -1,20 +1,18 @@
name: Tests
on:
push:
paths-ignore:
- '**.md'
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
on: [push, pull_request, workflow_dispatch]
permissions:
contents: read # to fetch code (actions/checkout)
jobs:
skip_duplicate_runs:
uses: ./.github/workflows/skip_duplicate_workflow_runs.yaml
rubocop:
name: 'Rubocop linting'
needs: skip_duplicate_runs
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@ -28,7 +26,7 @@ jobs:
test_mri:
name: >-
MRI: ${{ matrix.os }} ${{ matrix.ruby }}${{ matrix.no-ssl }}${{ matrix.yjit }}
needs: rubocop
needs: [rubocop, skip_duplicate_runs]
env:
CI: true
PUMA_TEST_DEBUG: true
@ -42,7 +40,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ ubuntu-20.04, ubuntu-18.04, macos-10.15, macos-11, macos-12, windows-2022 ]
os: [ ubuntu-20.04, ubuntu-18.04, macos-11, macos-12, windows-2022 ]
ruby: [ 2.4, 2.5, 2.6, 2.7, '3.0', 3.1, head ]
no-ssl: ['']
yjit: ['']
@ -57,21 +55,17 @@ jobs:
exclude:
- { os: windows-2022 , ruby: head }
- { os: macos-10.15 , ruby: 2.7 }
- { os: macos-10.15 , ruby: '3.0'}
- { os: macos-10.15 , ruby: 3.1 }
- { os: macos-10.15 , ruby: head }
- { os: macos-11 , ruby: 2.4 }
- { os: macos-11 , ruby: 2.5 }
- { os: macos-12 , ruby: 2.4 }
- { os: macos-12 , ruby: 2.5 }
- { os: macos-12 , ruby: 2.6 }
- { os: macos-11 , ruby: head }
steps:
- name: repo checkout
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
uses: actions/checkout@v3
- name: load ruby
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
uses: ruby/setup-ruby-pkgs@v1
with:
ruby-version: ${{ matrix.ruby }}
@ -84,35 +78,45 @@ jobs:
# fixes 'has a bug that prevents `required_ruby_version`'
- name: update rubygems for Ruby 2.4 - 2.5
if: contains('2.4 2.5', matrix.ruby)
if: |
contains('2.4 2.5', matrix.ruby) &&
(needs.skip_duplicate_runs.outputs.should_skip != 'true')
run: gem update --system 3.3.14 --no-document
continue-on-error: true
timeout-minutes: 5
- name: Compile Puma without SSL support
if: matrix.no-ssl == ' no SSL'
if: |
(matrix.no-ssl == ' no SSL') &&
(needs.skip_duplicate_runs.outputs.should_skip != 'true')
shell: bash
run: echo 'PUMA_DISABLE_SSL=true' >> $GITHUB_ENV
- name: set WERRORFLAG
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
shell: bash
run: echo 'PUMA_MAKE_WARNINGS_INTO_ERRORS=true' >> $GITHUB_ENV
- name: compile
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
run: bundle exec rake compile
- name: Use yjit
if: matrix.yjit == ' yjit'
if: |
(matrix.yjit == ' yjit') &&
(needs.skip_duplicate_runs.outputs.should_skip != 'true')
shell: bash
run: echo 'RUBYOPT=--yjit' >> $GITHUB_ENV
- name: test
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
timeout-minutes: 10
run: bundle exec rake test:all
test_non_mri:
name: >-
NON-MRI: ${{ matrix.os }} ${{ matrix.ruby }}${{ matrix.no-ssl }}
needs: rubocop
needs: [rubocop, skip_duplicate_runs]
env:
CI: true
TESTOPTS: -v
@ -131,22 +135,27 @@ jobs:
- { os: ubuntu-20.04 , ruby: jruby-head, allow-failure: true }
- { os: ubuntu-20.04 , ruby: truffleruby, allow-failure: true } # Until https://github.com/oracle/truffleruby/issues/2700 is solved
- { os: ubuntu-20.04 , ruby: truffleruby-head, allow-failure: true }
- { os: macos-10.15 , ruby: jruby }
- { os: macos-10.15 , ruby: truffleruby, allow-failure: true }
- { os: macos-11 , ruby: jruby }
- { os: macos-11 , ruby: truffleruby, allow-failure: true }
- { os: macos-12 , ruby: jruby }
- { os: macos-12 , ruby: truffleruby, allow-failure: true }
steps:
- name: repo checkout
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
uses: actions/checkout@v3
- name: set JAVA_HOME
if: startsWith(matrix.os, 'macos')
if: |
startsWith(matrix.os, 'macos') &&
(needs.skip_duplicate_runs.outputs.should_skip != 'true')
shell: bash
run: |
echo JAVA_HOME=$JAVA_HOME_11_X64 >> $GITHUB_ENV
- name: load ruby, ragel
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
uses: ruby/setup-ruby-pkgs@v1
with:
ruby-version: ${{ matrix.ruby }}
@ -157,22 +166,28 @@ jobs:
timeout-minutes: 10
- name: Compile Puma without SSL support
if: matrix.no-ssl == ' no SSL'
if: |
(matrix.no-ssl == ' no SSL') &&
(needs.skip_duplicate_runs.outputs.should_skip != 'true')
shell: bash
run: echo 'PUMA_DISABLE_SSL=true' >> $GITHUB_ENV
- name: set WERRORFLAG
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
shell: bash
run: echo 'PUMA_MAKE_WARNINGS_INTO_ERRORS=true' >> $GITHUB_ENV
- name: compile
if: ${{ needs.skip_duplicate_runs.outputs.should_skip != 'true' }}
run: bundle exec rake compile
- name: test
id: test
timeout-minutes: 12
continue-on-error: ${{ matrix.allow-failure || false }}
if: success() # only run if previous steps have succeeded
if: | # only run if previous steps have succeeded
success() &&
(needs.skip_duplicate_runs.outputs.should_skip != 'true')
run: bundle exec rake test:all
- name: >-

View File

@ -37,6 +37,7 @@ Sidekiq 7 (releasing soon) introduces Capsules, which allows you to run a Sideki
Check the following list to see if you're depending on any of these behaviors:
1. Configuration constants like `DefaultRackup` removed, see [#2928](https://github.com/puma/puma/pull/2928/files#diff-2dc4e3e83be7fd97cebc482ae07d6a8216944003de82458783fb00b5ae9524c8) for the full list.
1. We have changed the names of the following environment variables: `DISABLE_SSL` is now `PUMA_DISABLE_SSL`, `MAKE_WARNINGS_INTO_ERRORS` is now `PUMA_MAKE_WARNINGS_INTO_ERRORS`, and `WAIT_FOR_LESS_BUSY_WORKERS` is now `PUMA_WAIT_FOR_LESS_BUSY_WORKERS`.
1. Nakayoshi GC (`nakayoshi_fork` option in config) has been removed without replacement.
1. `wait_for_less_busy_worker` is now on by default. If you don't want to use this feature, you must add `wait_for_less_busy_worker false` in your config.
@ -44,7 +45,6 @@ Check the following list to see if you're depending on any of these behaviors:
1. We've removed the following constants: `Puma::StateFile::FIELDS`, `Puma::CLI::KEYS_NOT_TO_PERSIST_IN_STATE` and `Puma::Launcher::KEYS_NOT_TO_PERSIST_IN_STATE`, and `Puma::ControlCLI::COMMANDS`.
1. We no longer support Ruby 2.2, 2.3, or JRuby on Java 1.7 or below.
1. The behavior of `remote_addr` has changed. When using the set_remote_address header: "header_name" functionality, if the header is not passed, REMOTE_ADDR is now set to the physical peeraddr instead of always being set to 127.0.0.1. When an error occurs preventing the physical peeraddr from being fetched, REMOTE_ADDR is now set to the unspecified source address ('0.0.0.0') instead of to '127.0.0.1'
1. If you are creating your own Puma::Server objects, it's initialize signature has changed. In 5.x and below, it was `def initialize(app, events=Events.stdio, options={})`. Now it is `def initialize(app, log_writer=LogWriter.stdio, events=Events.new, options = {})`.
Then, update your Gemfile:

View File

@ -27,6 +27,8 @@ Newbies welcome! We would be happy to help you make your first contribution to a
Any questions about contributing may be asked in our [Discussions](https://github.com/puma/puma/discussions).
**If you're nervous, get stuck, need help, or want to know where to start and where you can help**, please don't hesitate to [book 30 minutes with maintainer @nateberkopec here](https://fantastical.app/nateberkopec/weekdays). He is happy to help!
#### Clone the repo
Clone the Puma repository:
@ -152,7 +154,7 @@ We find that values of 4000 or more work well. [Learn more about your file limit
Puma could use your help in several areas!
**Don't worry about "claiming an issue". No issues are "claimed" in Puma.** Just start working on it. Once you have a few lines of code, post a draft PR. We are more than happy to help once you have a draft PR up.
**Don't worry about "claiming an issue". No issues are "claimed" in Puma.** Just start working on it. The issue tracker is almost always kept updated, so if there is an open issue, it is ready for you to contribute (unless you have questions about how to close issue - then please ask!). Once you have a few lines of code, post a draft PR. We are more than happy to help once you have a draft PR up.
**New to systems programming? That's ok!** Puma deals with concepts you may not have been familiar with before, like sockets, TCP, UDP, SSL, and Threads. That's ok! You can learn by contributing. Also, see the "Bibliography" section at the end of this document.

View File

@ -1,4 +1,4 @@
## 6.0.0 / 2022-10-XX
## 6.0.0 / 2022-10-14
* Breaking Changes
* Dropping Ruby 2.2 and 2.3 support (now 2.4+) ([#2919])
@ -9,6 +9,9 @@
* Prefix all environment variables with `PUMA_` ([#2924], [#2853])
* Removed some constants ([#2957], [#2958], [#2959], [#2960])
* The following classes are now part of Puma's private API: `Client`, `Cluster::Worker`, `Cluster::Worker`, `HandleRequest`. ([#2988])
* Configuration constants like `DefaultRackup` removed ([#2928])
* Extracted `LogWriter` from `Events` ([#2798])
* Features
* Increase throughput on large (100kb+) response bodies by 3-10x ([#2896], [#2892])
@ -34,7 +37,6 @@
* Refactor
* log_writer.rb - add internal_write method ([#2888])
* [WIP] Refactor: Split out LogWriter from Events (no logic change) ([#2798])
* Extract prune_bundler code into it's own class. ([#2797])
* Refactor Launcher#run to increase readability (no logic change) ([#2795])
* Ruby 3.2 will have native IO#wait_* methods, don't require io/wait ([#2903])
@ -1915,6 +1917,7 @@ be added back in a future date when a java Puma::MiniSSL is added.
* Bugfixes
* Your bugfix goes here <Most recent on the top, like GitHub> (#Github Number)
[#2928]:https://github.com/puma/puma/pull/2928 "PR by @nateberkopec, merged 2022-09-10"
[#2919]:https://github.com/puma/puma/pull/2919 "PR by @MSP-Greg, merged 2022-08-30"
[#2652]:https://github.com/puma/puma/issues/2652 "Issue by @Roguelazer, closed 2022-09-04"
[#2653]:https://github.com/puma/puma/pull/2653 "PR by @Roguelazer, closed 2022-03-07"

View File

@ -11,7 +11,7 @@
Using "3.7.1" as a version example.
1. `bundle exec rake release`
2. `gem push --key github --host https://rubygems.pkg.github.com/puma pkg/puma-VERSION.gem`
3. Switch to latest JRuby version
4. `rake java gem`
5. `gem push pkg/puma-VERSION-java.gem`
1. Switch to latest JRuby version
1. `rake java gem`
1. `gem push pkg/puma-VERSION-java.gem`
1. Add release on Github at https://github.com/puma/puma/releases/new

View File

@ -250,6 +250,7 @@ module Puma
#
# * Set the socket backlog depth with +backlog+, default is 1024.
# * Set up an SSL certificate with +key+ & +cert+.
# * Set up an SSL certificate for mTLS with +key+, +cert+, +ca+ and +verify_mode+.
# * Set whether to optimize for low latency instead of throughput with
# +low_latency+, default is to not optimize for low latency. This is done
# via +Socket::TCP_NODELAY+.
@ -259,6 +260,8 @@ module Puma
# bind 'unix:///var/run/puma.sock?backlog=512'
# @example SSL cert
# bind 'ssl://127.0.0.1:9292?key=key.key&cert=cert.pem'
# @example SSL cert for mutual TLS (mTLS)
# bind 'ssl://127.0.0.1:9292?key=key.key&cert=cert.pem&ca=ca.pem&verify_mode=force_peer'
# @example Disable optimization for low latency
# bind 'tcp://0.0.0.0:9292?low_latency=false'
# @example Socket permissions

View File

@ -50,7 +50,7 @@ module Puma
@input << client
@selector.wakeup
true
rescue ClosedQueueError
rescue ClosedQueueError, IOError # Ignore if selector is already closed
false
end

View File

@ -48,6 +48,7 @@ module Puma
def handle_request(client, io_buffer, requests)
env = client.env
socket = client.io # io may be a MiniSSL::Socket
app_body = nil
return false if closed_socket?(socket)
@ -85,14 +86,18 @@ module Puma
begin
if SUPPORTED_HTTP_METHODS.include?(env[REQUEST_METHOD])
status, headers, res_body = @thread_pool.with_force_shutdown do
status, headers, app_body = @thread_pool.with_force_shutdown do
@app.call(env)
end
else
@log_writer.log "Unsupported HTTP method used: #{env[REQUEST_METHOD]}"
status, headers, res_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
status, headers, app_body = [501, {}, ["#{env[REQUEST_METHOD]} method is not supported"]]
end
# app_body needs to always be closed, hold value in case lowlevel_error
# is called
res_body = app_body
return :async if client.hijacked
status = status.to_i
@ -115,6 +120,17 @@ module Puma
status, headers, res_body = lowlevel_error(e, env, 500)
end
prepare_response(status, headers, res_body, io_buffer, requests, client)
ensure
io_buffer.reset
uncork_socket client.io
app_body.close if app_body.respond_to? :close
client.tempfile&.unlink
after_reply = env[RACK_AFTER_REPLY] || []
begin
after_reply.each { |o| o.call }
rescue StandardError => e
@log_writer.debug_error e
end unless after_reply.empty?
end
# Assembles the headers and prepares the body for actually sending the
@ -122,52 +138,50 @@ module Puma
#
# @param status [Integer] the status returned by the Rack application
# @param headers [Hash] the headers returned by the Rack application
# @param app_body [Array] the body returned by the Rack application or
# @param res_body [Array] the body returned by the Rack application or
# a call to `lowlevel_error`
# @param io_buffer [Puma::IOBuffer] modified in place
# @param requests [Integer] number of inline requests handled
# @param client [Puma::Client]
# @return [Boolean,:async]
def prepare_response(status, headers, app_body, io_buffer, requests, client)
def prepare_response(status, headers, res_body, io_buffer, requests, client)
env = client.env
socket = client.io
after_reply = env[RACK_AFTER_REPLY] || []
socket = client.io
return false if closed_socket?(socket)
resp_info = str_headers(env, status, headers, app_body, io_buffer, requests, client)
resp_info = str_headers(env, status, headers, res_body, io_buffer, requests, client)
# below converts app_body into body, dependent on app_body's characteristics, and
# resp_info[:content_length] will be set if it can be determined
if !resp_info[:content_length] && !resp_info[:transfer_encoding] && status != 204
if app_body.respond_to?(:to_ary)
if res_body.respond_to?(:to_ary)
length = 0
if array_body = app_body.to_ary
if array_body = res_body.to_ary
body = array_body.map { |part| length += part.bytesize; part }
elsif app_body.is_a?(::File) && app_body.respond_to?(:size)
length = app_body.size
elsif app_body.respond_to?(:each)
elsif res_body.is_a?(::File) && res_body.respond_to?(:size)
length = res_body.size
elsif res_body.respond_to?(:each)
body = []
app_body.each { |part| length += part.bytesize; body << part }
res_body.each { |part| length += part.bytesize; body << part }
end
resp_info[:content_length] = length
elsif app_body.is_a?(File) && app_body.respond_to?(:size)
resp_info[:content_length] = app_body.size
body = app_body
elsif app_body.respond_to?(:to_path) && app_body.respond_to?(:each) &&
File.readable?(fn = app_body.to_path)
elsif res_body.is_a?(File) && res_body.respond_to?(:size)
resp_info[:content_length] = res_body.size
body = res_body
elsif res_body.respond_to?(:to_path) && res_body.respond_to?(:each) &&
File.readable?(fn = res_body.to_path)
body = File.open fn, 'rb'
resp_info[:content_length] = body.size
else
body = app_body
body = res_body
end
elsif !app_body.is_a?(::File) && app_body.respond_to?(:to_path) && app_body.respond_to?(:each) &&
File.readable?(fn = app_body.to_path)
elsif !res_body.is_a?(::File) && res_body.respond_to?(:to_path) && res_body.respond_to?(:each) &&
File.readable?(fn = res_body.to_path)
body = File.open fn, 'rb'
resp_info[:content_length] = body.size
else
body = app_body
body = res_body
end
line_ending = LINE_END
@ -175,8 +189,8 @@ module Puma
content_length = resp_info[:content_length]
keep_alive = resp_info[:keep_alive]
if app_body && !app_body.respond_to?(:each)
response_hijack = app_body
if res_body && !res_body.respond_to?(:each)
response_hijack = res_body
else
response_hijack = resp_info[:response_hijack]
end
@ -210,18 +224,6 @@ module Puma
fast_write_response socket, body, io_buffer, chunked, content_length.to_i
keep_alive
ensure
io_buffer.reset
resp_info = nil
uncork_socket socket
app_body.close if app_body.respond_to? :close
client.tempfile&.unlink
begin
after_reply.each { |o| o.call }
rescue StandardError => e
@log_writer.debug_error e
end unless after_reply.empty?
end
# @param env [Hash] see Puma::Client#env, from request

View File

@ -184,7 +184,7 @@ Minitest::Test.include TestSkips
class Minitest::Test
REPO_NAME = ENV['GITHUB_REPOSITORY'] ? ENV['GITHUB_REPOSITORY'][/[^\/]+\z/] : 'puma'
PROJECT_ROOT = File.dirname(__dir__)
def self.run(reporter, options = {}) # :nodoc:
prove_it!
@ -259,3 +259,16 @@ module AggregatedResults
end
end
Minitest::SummaryReporter.prepend AggregatedResults
module TestTempFile
require "tempfile"
def tempfile_create(basename, data, mode: File::BINARY)
fio = Tempfile.create(basename, mode: mode)
fio.write data
fio.flush
fio.rewind
@ios << fio
fio
end
end
Minitest::Test.include TestTempFile

View File

@ -10,7 +10,7 @@ class TestBundlePruner < Minitest::Test
dirs = bundle_pruner.send(:paths_to_require_after_prune)
assert_equal(2, dirs.length)
assert_match(%r{#{REPO_NAME}/lib$}, dirs[0]) # lib dir
assert_equal(File.join(PROJECT_ROOT, "lib"), dirs[0]) # lib dir
assert_match(%r{puma-#{Puma::Const::PUMA_VERSION}$}, dirs[1]) # native extension dir
refute_match(%r{gems/minitest-[\d.]+/lib$}, dirs[2])
end
@ -21,7 +21,7 @@ class TestBundlePruner < Minitest::Test
dirs = bundle_pruner([], ['minitest']).send(:paths_to_require_after_prune)
assert_equal(3, dirs.length)
assert_match(%r{#{REPO_NAME}/lib$}, dirs[0]) # lib dir
assert_equal(File.join(PROJECT_ROOT, "lib"), dirs[0]) # lib dir
assert_match(%r{puma-#{Puma::Const::PUMA_VERSION}$}, dirs[1]) # native extension dir
assert_match(%r{gems/minitest-[\d.]+/lib$}, dirs[2]) # minitest dir
end

View File

@ -112,7 +112,7 @@ class TestIntegrationSingle < TestIntegration
rejected_curl_wait_thread.join
assert_match(/Slept 10/, curl_stdout.read)
assert_match(/Connection refused/, rejected_curl_stderr.read)
assert_match(/Connection refused|Couldn't connect to server/, rejected_curl_stderr.read)
Process.wait(@server.pid)
@server.close unless @server.closed?

View File

@ -5,6 +5,11 @@ require "net/http"
require "nio"
require "ipaddr"
class WithoutBacktraceError < StandardError
def backtrace; nil; end
def message; "no backtrace error"; end
end
class TestPumaServer < Minitest::Test
parallelize_me!
@ -26,6 +31,7 @@ class TestPumaServer < Minitest::Test
@ios.each do |io|
begin
io.close if io.respond_to?(:close) && !io.closed?
File.unlink io.path if io.is_a? File
rescue Errno::EBADF
ensure
io = nil
@ -53,6 +59,11 @@ class TestPumaServer < Minitest::Test
header
end
# only for shorter bodies!
def send_http_and_sysread(req)
send_http(req).sysread 2_048
end
def send_http_and_read(req)
send_http(req).read
end
@ -140,28 +151,33 @@ class TestPumaServer < Minitest::Test
data = send_http_and_read "GET / HTTP/1.0\r\nConnection: close\r\n\r\n"
assert_equal "Hello World", data.split("\n").last
assert_equal "Hello World", data.split("\r\n\r\n", 2).last
end
def test_file_body
random_bytes = SecureRandom.random_bytes(4096 * 32)
path = Tempfile.open { |f| f.path }
File.binwrite path, random_bytes
server_run { |env| [200, {}, File.open(path, 'rb')] }
tf = tempfile_create("test_file_body", random_bytes)
server_run { |env| [200, {}, tf] }
data = +''
skt = send_http("GET / HTTP/1.1\r\nHost: [::ffff:127.0.0.1]:#{@port}\r\n\r\n")
data << skt.sysread(65_536) while skt.wait_readable(0.1)
data = send_http_and_read "GET / HTTP/1.0\r\nHost: [::ffff:127.0.0.1]:9292\r\n\r\n"
ary = data.split("\r\n\r\n", 2)
assert_equal random_bytes.bytesize, ary.last.bytesize
assert_equal random_bytes, ary.last
ensure
File.delete(path) if File.exist?(path)
tf.close
end
def test_file_to_path
random_bytes = SecureRandom.random_bytes(4096 * 32)
path = Tempfile.open { |f| f.path }
File.binwrite path, random_bytes
tf = tempfile_create("test_file_to_path", random_bytes)
path = tf.path
obj = Object.new
obj.singleton_class.send(:define_method, :to_path) { path }
@ -169,16 +185,17 @@ class TestPumaServer < Minitest::Test
server_run { |env| [200, {}, obj] }
data = send_http_and_read "GET / HTTP/1.0\r\nHost: [::ffff:127.0.0.1]:9292\r\n\r\n"
data = +''
skt = send_http("GET / HTTP/1.1\r\nHost: [::ffff:127.0.0.1]:#{@port}\r\n\r\n")
data << skt.sysread(65_536) while skt.wait_readable(0.1)
ary = data.split("\r\n\r\n", 2)
assert_equal random_bytes.bytesize, ary.last.bytesize
assert_equal random_bytes, ary.last
ensure
File.delete(path) if File.exist?(path)
tf.close
end
def test_proper_stringio_body
data = nil
@ -407,36 +424,54 @@ EOF
assert_match(/{}\n$/, data)
end
def test_lowlevel_error_message
@server = Puma::Server.new @app, @events, {log_writer: @log_writer, :force_shutdown_after => 2}
class ArrayClose < Array
attr_reader :is_closed
def closed?
@is_closed
end
server_run do
def close
@is_closed = true
end
end
# returns status as an array, which throws lowlevel error
def test_lowlevel_error_body_close
app_body = ArrayClose.new(['lowlevel_error'])
server_run(log_writer: @log_writer, :force_shutdown_after => 2) do
[[0,1], {}, app_body]
end
data = send_http_and_sysread "GET / HTTP/1.0\r\n\r\n"
assert_includes data, 'HTTP/1.0 500 Internal Server Error'
assert_includes data, "Puma caught this error: undefined method `to_i' for [0, 1]:Array"
refute_includes data, 'lowlevel_error'
sleep 0.1 unless ::Puma::IS_MRI
assert app_body.closed?
end
def test_lowlevel_error_message
server_run(log_writer: @log_writer, :force_shutdown_after => 2) do
raise NoMethodError, "Oh no an error"
end
data = send_http_and_read "GET / HTTP/1.0\r\n\r\n"
data = send_http_and_sysread "GET / HTTP/1.0\r\n\r\n"
assert_match(/HTTP\/1.0 500 Internal Server Error/, data)
assert_includes data, 'HTTP/1.0 500 Internal Server Error'
assert_match(/Puma caught this error: Oh no an error.*\(NoMethodError\).*test\/test_puma_server.rb/m, data)
end
class WithoutBacktraceError < StandardError
def backtrace; nil; end
def message; "no backtrace error"; end
def class; "WithoutBacktraceError"; end
end
def test_lowlevel_error_message_without_backtrace
@server = Puma::Server.new @app, @events, {log_writer: @log_writer, :force_shutdown_after => 2}
server_run do
server_run(log_writer: @log_writer, :force_shutdown_after => 2) do
raise WithoutBacktraceError.new
end
data = send_http_and_read "GET / HTTP/1.1\r\n\r\n"
assert_match(/HTTP\/1.1 500 Internal Server Error/, data)
assert_match(/Puma caught this error: no backtrace error.*\(WithoutBacktraceError\)/, data)
assert_match(/<no backtrace available>/, data)
data = send_http_and_sysread "GET / HTTP/1.1\r\n\r\n"
assert_includes data, 'HTTP/1.1 500 Internal Server Error'
assert_includes data, 'Puma caught this error: no backtrace error (WithoutBacktraceError)'
assert_includes data, '<no backtrace available>'
end
def test_force_shutdown_error_default
@ -1433,7 +1468,7 @@ EOF
# TODO: it would be great to test a connection from a non-localhost IP, but we can't really do that. For
# now, at least test that it doesn't return garbage.
remote_addr = send_http_and_read("GET / HTTP/1.1\r\n\r\n").split("\r\n").last
remote_addr = send_http_and_sysread("GET / HTTP/1.1\r\n\r\n").split("\r\n").last
assert_equal @host, remote_addr
end
end

View File

@ -210,6 +210,43 @@ class TestRackServer < Minitest::Test
assert_equal str_ary_bytes, content_length
end
def test_hijack_body_close
available = true
@server.app = ->(env) {
if available
available = false
hijack_lambda = ->(io) {
io.syswrite 'hijacked'
io.close
}
[200, { 'Content-Type' => 'text/plain', 'rack.hijack' => hijack_lambda},
::Rack::BodyProxy.new([]) { available = true }]
else
[500, { 'Content-Type' => 'text/plain' }, ['incorrect']]
end
}
@server.run
socket1 = TCPSocket.new "127.0.0.1", @port
socket1.syswrite "GET / HTTP/1.1\r\n\r\n"
sleep 0.25 if Puma::IS_WINDOWS || !Puma::IS_MRI
resp1 = socket1.sysread 1_024
sleep 0.01 # time for close block to be called ?
socket2 = TCPSocket.new "127.0.0.1", @port
socket2.syswrite "GET / HTTP/1.1\r\n\r\n"
sleep 0.25 if Puma::IS_WINDOWS || !Puma::IS_MRI
resp2 = socket2.sysread 1_024
assert_operator resp1, :end_with?, 'hijacked'
assert_operator resp2, :end_with?, 'hijacked'
socket1.close
socket2.close
end
def test_common_logger
log = StringIO.new
@ -259,5 +296,4 @@ class TestRackServer < Minitest::Test
assert_includes headers.downcase, TRANSFER_ENCODING_CHUNKED
assert_equal STR_1KB * 10, resp_body
end
end