Skip to content

ngx_mruby v2.7.3

1. Nginx::SSL::ACME::Dehydrated#auto_cert_deploy reads the standard error of dehydrated, so that it accepts the terms of service of the CA, and accepts them at most once per call (#636)

Section titled “1. Nginx::SSL::ACME::Dehydrated#auto_cert_deploy reads the standard error of dehydrated, so that it accepts the terms of service of the CA, and accepts them at most once per call (#636)”
  • Before: #auto_cert_deploy of the auto-ssl mrbgem read only the standard output of dehydrated. dehydrated 0.5.0 and later write the request to accept the terms of service of the CA to standard error, so the method did not see it and did not call #auto_accept_terms. With a dehydrated account that was not registered yet, dehydrated stopped, no certificate was stored, and the handshake failed: Nginx::SSL#certificate_data= raised TypeError for the missing certificate. What dehydrated wrote to standard error reached error.log as raw lines, without a time or a level. With dehydrated 0.4.0, which writes the request to standard output, the method called #auto_accept_terms and then itself again, with no limit: when the registration failed, the next run asked again, and the method ran dehydrated in the handshake until the mruby stack was exhausted.
  • Now: #auto_cert_deploy runs dehydrated with its standard error joined to its standard output (2>&1). It sees the request, #auto_accept_terms registers the account (dehydrated --register --accept-terms), and #auto_cert_deploy runs dehydrated once more, which gets the certificate. It accepts the terms at most once per call: when the second run asks again, because the registration failed, the method returns without a certificate after two runs of dehydrated. All that --cron writes, standard error included, is in the [info] lines that the method writes with Nginx::SSL.log. The method returns nil, as before.
  • Why it changed:
    • The defect: the tests of the auto-ssl mrbgem that this pull request adds (test/t/cases/auto_ssl.rb, with dehydrated 0.7.2 and the ACME test server Pebble) found that a new account never got a certificate. The method was written for dehydrated 0.4.0, the version in test/conf/auto-ssl/dehydrated, which wrote the request to standard output. The review of the pull request found that a failed registration then made the method run dehydrated without end: dehydrated 0.7.2 removes the account key that it made when the registration fails, so every run asks for the terms again. The tests reproduce it with a Pebble that requires external account binding.
    • Why the old behavior could not be kept: the method finds the request in what dehydrated printed. With standard error kept apart, it cannot find it with any dehydrated that speaks ACME v2 (0.6.0 and later), and ACME v2 is the only version that Let’s Encrypt has served since it turned off ACME v1 in 2021. Once the method finds the request, a CA that refuses the registration (one that requires external account binding, an outage, a rate limit on new accounts) would keep the worker in the handshake without the limit.
    • What the documentation promised: docs/class_and_method/README.md refers to the scripts in mrbgems/auto-ssl/mrblib for the API and says nothing about the output of dehydrated. #auto_accept_terms and the check in #auto_cert_deploy exist to accept the terms of service on the first run.
  • Affected: TLS handshake handlers that call Nginx::SSL::ACME::Dehydrated#auto_cert_deploy, as test/conf/auto-ssl/nginx.conf.dehydrated_example does. With an error_log level above info (the examples use notice), the messages that dehydrated --cron writes to standard error, its errors included, no longer appear in error.log.
  • What to do: nothing to get a certificate with a new account. To see the errors of dehydrated --cron in error.log, set the level of the error_log that the server uses to info. Code that relied on the method retrying the registration within one call calls it again in a later handshake, which runs dehydrated again.
  • Not changed by this release: #auto_accept_terms still reads only the standard output of dehydrated --register --accept-terms; what that command writes to standard error, such as “Error registering account key”, still reaches error.log as raw lines.

2. A content handler that calls Nginx::Async.sleep answers after the sleep (#646)

Section titled “2. A content handler that calls Nginx::Async.sleep answers after the sleep (#646)”
  • Before: mruby_content_handler* code could call Nginx::Async.sleep without an exception, but nginx did not wait for it: the request ended at the sleep, the client got no response, and the code after the sleep did not run. The connection was closed, or kept open for the next request of the client without an answer to this one. A subrequest to a location whose content handler sleeps, for example one that Nginx::Async::HTTP.sub_request sends, got the output of that handler, but nginx then logged http request count is zero at level alert, and the connection did not answer the next request of the client.
  • Now: nginx waits for a content handler that sleeps. The code after the sleep runs, and the request gets what the handler writes with Nginx.rputs or Nginx.echo, or the status that it returns with Nginx.return, as when the handler does not sleep. A handler that sleeps more than once answers after its last sleep. When the handler raises after the sleep, the request gets 500, as in the other handlers. A subrequest to such a location gets its output, and the connection goes on. After the sleep, Nginx::Async::HTTP.sub_request raises a RuntimeError in a content handler, so the request gets 500: nginx would not finish the request after that subrequest. In 2.7.2 that code did not run.
  • Why it changed:
    • The defect: the content phase finalized the request with the NGX_DONE that the handler returned while its fiber waited, so nginx freed the request before the timer of the sleep fired, and the cleanup of the request deleted that timer. The 2.7.1 release notes list this under Known limitations and plan a release that lets a content handler wait; the owner decided on 2026-10-04 that the content handler supports Nginx::Async.sleep instead of raising.
    • Why the old behavior could not be kept: the code after the sleep can run only if the request stays until the sleep ends, and a request that stays is answered. The other way to remove the defect was to raise a RuntimeError in the content handler, as in a log handler, which would break code that calls the method there without an exception today.
    • What the documentation promised: docs/class_and_method/README.md said that the method does not raise in a content handler and that the request ends at the sleep without a response, and it listed the set, rewrite and access handlers as the places where the method works.
  • Affected: mruby_content_handler and mruby_content_handler_code code that calls Nginx::Async.sleep, and subrequests to a location with such code. Such a request now stays open for the time of the sleep and then gets a response.
  • What to do: nothing for code that expects its response after the sleep. Code that relied on the sleep to end the request without a response gets a response now: to close the connection without one, return Nginx::HTTP_CLOSE (444) with Nginx.return instead. Send a subrequest from a rewrite or access handler, not after a sleep in a content handler.
  • Not changed by this release: a content handler that runs in the location that Nginx.redirect runs from Ruby code still ends the request at the sleep, without its output. The handler that called Nginx.redirect goes on after the call and may answer the request itself, and the output of the sleeping handler would follow that response.

3. An exception in the location that Nginx.redirect runs ends the handler of that location, and nginx finishes the request (#646)

Section titled “3. An exception in the location that Nginx.redirect runs ends the handler of that location, and nginx finishes the request (#646)”
  • Before: Nginx.redirect "/path" runs the handlers of /path before it returns. When an mruby handler or filter of /path raised, for example its content handler, rewrite handler or header filter, the exception reached the code that called Nginx.redirect as if the call had raised it, and the code after the call did not run. When that code did not rescue the exception, the calling handler answered 500; when it rescued it, the client got no response. In both cases nginx did not finish the request: the connection stayed open after the response until the client closed it, the access log got no line for the request, and stub_status counted the connection as active until the worker exited.
  • Now: the exception ends the handler of /path as it does without the redirect, and the request gets 500 unless its response was already started. Nginx.redirect returns, and the code after it runs, as when /path does not raise. nginx finishes the request, writes its access log line and closes the connection. When the handler that called Nginx.redirect ends with the 500 that /path left in the status, nginx tries to send that status again and logs header already sent at level alert, as it already does when a handler returns a status after the response of the redirect target. The error.log lines of the exception stay the same, except that an exception in a header filter is logged once instead of twice.
  • Why it changed:
    • The defect: mruby passes the exception that ends a fiber resumed from C by longjmp to the innermost running mruby VM. The handler of /path runs in a fiber of its own inside the Ruby call of the handler that called Nginx.redirect, so the exception jumped over the nginx functions between the two (ngx_http_internal_redirect and the checkers of the phases), which finish the request. The tests of this pull request, test/t/cases/redirect_to_raising_handler.rb, show the connection that stays open.
    • Why the old behavior could not be kept: nginx finishes the request only in the functions that the exception jumped over, so the request could not end while the exception reached the calling code. Stopping the exception at the handler of /path lets the request end, and the calling code then goes on as it does after a redirect to a location that does not raise.
    • What the documentation promised: docs/class_and_method/README.md said that the other location returns the response to the client, and said nothing about an exception in it.
  • Affected: code that calls Nginx.redirect with an internal location whose mruby handlers or filters raise, and code that rescued such an exception around Nginx.redirect. Such code no longer sees the exception, and the code after the call runs.
  • What to do: nothing for code that did not rescue the exception. Code that rescued it around Nginx.redirect to react to a failure of the redirect target moves the rescue into the code of that location.
  • Not changed by this release: when the handler that called Nginx.redirect answers the request, with a status or an exception, while a rewrite handler of the redirect target still sleeps in Nginx::Async.sleep, the output of that rewrite handler follows the response on the connection when its timer fires.

4. A stream session goes on or ends after Nginx::Stream::Async.sleep by the result of its code, and an exception after the sleep no longer aborts the next session (#651)

Section titled “4. A stream session goes on or ends after Nginx::Stream::Async.sleep by the result of its code, and an exception after the sleep no longer aborts the next session (#651)”
  • Before: after Nginx::Stream::Async.sleep, only code that left Nginx::Stream::Connection.stream_status at Nginx::Stream::DECLINED, the default, let the session go on. When the code raised, or set the status to Nginx::Stream::ABORT, Nginx::Stream::OK or another value, nginx neither ended nor continued the session: the client got nothing until it closed the connection, and the session stayed until the worker exited. An exception after the sleep also stayed in the mruby state that the sessions of the stream {} block share, and the next session that the worker handled, in any server of the block, was aborted before its code ran; error.log logged the earlier exception again for that session.
  • Now: after the sleep, the session goes on or ends as it does when the code does not sleep: Nginx::Stream::DECLINED goes on with the next handler, Nginx::Stream::OK with the next phase, Nginx::Stream::DONE and Nginx::Stream::AGAIN leave the session waiting, and an exception, Nginx::Stream::ABORT or another status ends the session. An exception after the sleep is logged once, for its own session, and the next session runs its code.
  • Why it changed:
    • The defect: nginx’s checker of the access phase handles the result of a handler that returns at once. A handler that sleeps returns NGX_DONE to it, and the timer that resumes the code dropped the result of the code after the sleep, except for NGX_DECLINED. After an exception it did not clear the exception from the shared mruby state either, as the code without the sleep does, and ngx_stream_mrb_start_fiber of the next session read it as its own. The HTTP handlers got the same clearing in 2.7.1 (#550).
    • Why the old behavior could not be kept: a session whose result is dropped is never ended or continued, and an exception that stays in the shared state ends a session that did not raise it. Handling the result as nginx’s checker does is what ends or continues the session.
    • What the documentation promised: docs/class_and_method/README.md documented Nginx::Stream::Async.sleep for the access phase and Nginx::Stream::Connection.stream_status= with Nginx::Stream::ABORT to deny a client, and said nothing about the result after the sleep.
  • Affected: mruby_stream and mruby_stream_code code that calls Nginx::Stream::Async.sleep and then raises, or sets Nginx::Stream::Connection.stream_status to a value other than Nginx::Stream::DECLINED; and the next stream session that the worker handled after such an exception.
  • What to do: nothing for code that sets Nginx::Stream::ABORT after the sleep to deny a client: the session now ends. Code that set Nginx::Stream::OK after the sleep now goes on with the next phase, for example proxy_pass; set Nginx::Stream::ABORT instead where the session must not go on.

5. Nginx::Upstream and Nginx::Stream::Connection raise TypeError for an argument that is not a String (#652)

Section titled “5. Nginx::Upstream and Nginx::Stream::Connection raise TypeError for an argument that is not a String (#652)”
  • Before: Nginx::Upstream.new, Nginx::Upstream#server=, Nginx::Stream::Connection.new and Nginx::Stream::Connection#upstream_server= read their argument as a String without checking its class. With nil, the two setters read through a null pointer, and the worker process stopped with a segmentation fault. With another object that is not a String, the result depended on how mruby stores that object: the worker could stop in the same way, or the method read the memory of the object as if it were the bytes of a string.
  • Now: the four methods raise TypeError (“… cannot be converted to String”) for an argument that is not a String, before they look up the upstream or change its server. A String, also an instance of a subclass of String, works as before.
  • Why it changed:
    • The defect: the methods took the argument with mrb_get_args(mrb, "o", ...) and read it with RSTRING_LEN and RSTRING_PTR, which are valid only for a String (src/http/ngx_http_mruby_upstream.c, src/stream/ngx_stream_mruby_connection.c). The tests of this pull request, test/t/cases/upstream_server_checks.rb and test/t/cases/stream_upstream_server_checks.rb, pass nil. The owner decided on 2026-10-29 to fix it in 2.7.3 and to record it as a behavior change: 「直していいけどその仕様前提で書いてる人もいるから振る舞いの変更としてちゃんとリリースに書くようにしてね」 (“fix it, but some people write code that assumes this behavior, so record it properly as a behavior change in the release”).
    • Why the old behavior could not be kept: an object that is not a String has no bytes of a name or of an address to read, so the methods cannot give it a meaning; checking the class first is what keeps them from reading other memory.
    • What the documentation promised: docs/class_and_method/README.md described the argument as a string (Nginx::Upstream#new(string), Nginx::Upstream#server=(string)) and said nothing about other classes.
  • Affected: code that passes anything but a String to these methods, for example nil from a missing key of a Hash, or a Symbol.
  • What to do: pass a String, for example with to_s, and handle a missing value before the call.

6. Nginx::Upstream.new and Nginx::Stream::Connection.new find the upstream by its whole name (#652)

Section titled “6. Nginx::Upstream.new and Nginx::Stream::Connection.new find the upstream by its whole name (#652)”
  • Before: the two methods compared the name of each upstream {} block with the argument only over the length of the argument, case-insensitively, and took the first that matched. A name that only started the name of an upstream found that upstream: Nginx::Upstream.new("back") found upstream backend, and Nginx::Upstream.new("backend") found upstream backend_a when that block came first in the configuration.
  • Now: the methods find the upstream whose whole name is the argument, compared case-insensitively as nginx compares upstream names. A name that only starts the name of an upstream raises RuntimeError (<name> not found upstream config), as an unknown name did before.
  • Why it changed:
    • The defect: the comparison was ngx_strncasecmp over the length of the argument, in ngx_mrb_upstream_init and ngx_stream_mrb_connection_init. It was found when the documentation of #648 was checked against the code, and test/t/cases/filter_connection.rb pinned it as the behavior of 2.x. The owner decided on 2026-10-29 that 2.7.3 finds the upstream by its whole name (the option “2.7.3で完全一致にする (推奨)”, “use the whole name in 2.7.3 (recommended)”), and approved the change of that expectation (the option “変えてよい (推奨)”, “it may change (recommended)”).
    • Why the old behavior could not be kept: with the prefix, a script that named one upstream could get another one, and Nginx::Upstream#server= then changed the server of that other upstream.
    • What the documentation promised: the documentation called the argument the name of the upstream ("YOUR_UPSTREAM_NAME") and said nothing about a part of a name.
  • Affected: scripts that pass a part of the name of an upstream block, and scripts that name an upstream whose name starts the name of another upstream defined before it.
  • What to do: pass the whole name of the upstream {} block.

7. Nginx::Upstream#server= and Nginx::Stream::Connection#upstream_server= raise for an upstream with a zone (#652)

Section titled “7. Nginx::Upstream#server= and Nginx::Stream::Connection#upstream_server= raise for an upstream with a zone (#652)”
  • Before: for an upstream {} block with a zone, the two setters changed the peer in the shared memory of the zone to point at the memory of the worker that called them. With one worker (worker_processes 1) the change worked; with more workers, the change was not valid in the other workers, as “Not changed by this release” of the Nginx::Upstream#server= entry of the 2.7.2 release notes says.
  • Now: both setters raise RuntimeError (... does not support an upstream with a zone) before they change anything, also with worker_processes 1.
  • Why it changed:
    • The defect: the setters store the name and the address of the server in the memory of one worker, which only that worker can read, and point the shared peer at it. The owner decided on 2026-10-29 to refuse a zone upstream in 2.7.3 (the option “2.7.3で例外にする (推奨)”, “raise in 2.7.3 (recommended)”, for question 1 of the proposal docs/proposals/upstream-server-setters.md on next).
    • Why the old behavior could not be kept: a change that is valid for every worker needs the slab of the zone, and nginx itself replaces the peers of a zone upstream when a server has the resolve parameter (nginx 1.27.3 and later), which an in-place change would work against. Refusing the zone case is the change that the 2.x line can make.
    • What the documentation promised: the 2.7.2 release notes called the zone case unsupported, and docs/class_and_method/README.md said that the setters do not check for a zone.
  • Affected: configurations whose scripts call one of the two setters for an upstream block with zone, also those with one worker.
  • What to do: remove zone from an upstream block that a script changes with these setters; their change applies to one worker either way.

None.

  • Nginx::Stream::Async.sleep no longer leaves its timer armed on the freed connection pool when a stream session’s pool is destroyed while the sleep is still pending (a worker shutting down or a configuration reload, or a connection timeout). ngx_stream_mrb_async_sleep armed the timer (ctx->sleep, an ngx_event_t in the connection pool) and registered no cleanup on that pool, so the freed pool was read when the orphaned timer fired (use-after-free). A client that closes its connection during the sleep does not reach it: nginx defers the close until the handler resumes. ngx_stream_mrb_start_fiber now registers a connection pool cleanup that cancels a pending sleep timer and unregisters the fiber when the session ends, as the HTTP Nginx::Async.sleep path already did, and the fiber is registered with the GC once per fiber instead of once per Nginx::Stream::Async.sleep call (a fiber that slept more than once had stayed on the GC root). The change is in src/stream only; no directive, Ruby method or build option changed (#645).
  • The 2.x installation guide now uses the released v2.7.2 tag and a complete loopback HTTP trial. It distinguishes compilation from make install, uses the configured prefix consistently, includes nginx validation/reload/quit, and records the actual static-build verification conditions. The historical Docker image is identified separately; no runtime behavior changes.

  • Tests of the auto-ssl mrbgem with Redis, the ACME test server Pebble and dehydrated: test/t/cases/auto_ssl.rb runs when NGX_MRUBY_TEST_AUTO_SSL names the directory that sh test/auto_ssl/deps.sh start DIR prepared, and CI runs it in every cell of the build matrix. It uses the new test ports 18171 and 18172 of the test nginx and 18173 and 18174 of a second nginx that it starts; the services listen on 6379, 8053, 8055, 14000, 14001, 15000 and 15001 (see “Testing the auto-ssl mrbgem” in docs/test/README.md) (#636)

  • nginx 1.31.8 and 1.30.6: nginx_version, which build.sh and test.sh read, names nginx 1.31.8 instead of 1.31.6. CI builds ngx_mruby with nginx 1.31.8, 1.30.6, 1.28.3 and 1.26.3 instead of 1.31.6, 1.30.5, 1.28.3 and 1.26.3, as a static and as a dynamic module of each; the build with AddressSanitizer and UndefinedBehaviorSanitizer uses 1.31.8. nginx 1.31.7 and 1.30.6 fix the vulnerabilities in nginx that the nginx security advisories list as CVE-2026-15176, CVE-2026-93479, CVE-2026-95101, CVE-2026-97629 and CVE-2026-97205. The nginx 1.31.8 that build.sh downloads by default has these fixes; a build that names its own nginx source with NGINX_SRC_ENV uses the nginx it names. ngx_mruby’s code did not change for these versions. nginx’s own changes are in its CHANGES and CHANGES-1.30; among them, the HTTP/2 module of 1.31.7 uses the RFC 9218 prioritization scheme and ignores RFC 7540 priority signals, and 1.31.8 resolves the path of the working_directory directive against the nginx prefix instead of the current directory (#638).

  • The 2.x pages of the site: on every pull request to master, the new workflow site-v2.yml builds the site of next with the pull request’s docs/ under /v2/ and fails on a broken link of the 2.x pages; on every push to master, the new workflow site-dispatch.yml starts the site’s workflow on next. Neither is in the needs of ci-ok (“The 2.x pages of the site” in docs/test/README.md) (#656).

Supported nginx versions: the nginx mainline and stable lines (see README.md); CI builds nginx 1.31.8, 1.30.6, 1.28.3 and 1.26.3.

  • No directive was added, renamed or removed. Rebuild the module with this release the way you built 2.7.2, and replace the binary or the dynamic module as usual.

Check these before upgrading. Each line is the “What to do” of its entry above.

  • Nginx::SSL::ACME::Dehydrated#auto_cert_deploy: nothing to get a certificate with a new account. To see the errors of dehydrated --cron in error.log, set the level of the error_log that the server uses to info. Code that relied on the method retrying the registration within one call calls it again in a later handshake (entry 1).
  • Nginx::Async.sleep in a content handler: nothing for code that expects its response after the sleep. Code that relied on the sleep to end the request without a response returns Nginx::HTTP_CLOSE (444) with Nginx.return instead. A subrequest is sent from a rewrite or access handler, not after a sleep in a content handler (entry 2).
  • An exception in the location that Nginx.redirect runs: nothing for code that did not rescue it. Code that rescued it around Nginx.redirect moves the rescue into the code of that location (entry 3).
  • The result of a stream session after Nginx::Stream::Async.sleep: nothing for code that sets Nginx::Stream::ABORT to deny a client. Code that set Nginx::Stream::OK after the sleep sets Nginx::Stream::ABORT instead where the session must not go on (entry 4).
  • Nginx::Upstream and Nginx::Stream::Connection with an argument that is not a String: pass a String, for example with to_s, and handle a missing value before the call (entry 5).
  • Nginx::Upstream.new and Nginx::Stream::Connection.new with a part of a name: pass the whole name of the upstream {} block (entry 6).
  • The upstream server setters with a zone: remove zone from an upstream block that a script changes with them (entry 7).