ngx_mruby v2.7.3
Behavior changes: read before upgrading
Section titled “Behavior changes: read before upgrading”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_deployof 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=raisedTypeErrorfor the missing certificate. What dehydrated wrote to standard error reachederror.logas 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_termsand 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_deployruns dehydrated with its standard error joined to its standard output (2>&1). It sees the request,#auto_accept_termsregisters the account (dehydrated --register --accept-terms), and#auto_cert_deployruns 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--cronwrites, standard error included, is in the[info]lines that the method writes withNginx::SSL.log. The method returnsnil, 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 intest/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_ refers to the scripts inmethod/README.md mrbgems/auto-ssl/mrblibfor the API and says nothing about the output of dehydrated.#auto_accept_termsand the check in#auto_cert_deployexist to accept the terms of service on the first run.
- The defect: the tests of the auto-ssl mrbgem that this pull request adds
(
- Affected: TLS handshake handlers that call
Nginx::SSL::ACME::Dehydrated#auto_cert_deploy, astest/conf/auto-ssl/nginx.conf.dehydrated_exampledoes. With anerror_loglevel aboveinfo(the examples usenotice), the messages thatdehydrated --cronwrites to standard error, its errors included, no longer appear inerror.log. - What to do: nothing to get a certificate with a new account. To see the
errors of
dehydrated --croninerror.log, set the level of theerror_logthat the server uses toinfo. 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_termsstill reads only the standard output ofdehydrated --register --accept-terms; what that command writes to standard error, such as “Error registering account key”, still reacheserror.logas 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 callNginx::Async.sleepwithout 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 thatNginx::Async::HTTP.sub_requestsends, got the output of that handler, but nginx then loggedhttp request count is zeroat levelalert, 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.rputsorNginx.echo, or the status that it returns withNginx.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_requestraises aRuntimeErrorin 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_DONEthat 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 supportsNginx::Async.sleepinstead 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
RuntimeErrorin 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_ 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.method/README.md
- The defect: the content phase finalized the request with the
- Affected:
mruby_content_handlerandmruby_content_handler_codecode that callsNginx::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) withNginx.returninstead. 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.redirectruns from Ruby code still ends the request at the sleep, without its output. The handler that calledNginx.redirectgoes 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/pathbefore it returns. When an mruby handler or filter of/pathraised, for example its content handler, rewrite handler or header filter, the exception reached the code that calledNginx.redirectas 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
/pathas it does without the redirect, and the request gets 500 unless its response was already started.Nginx.redirectreturns, and the code after it runs, as when/pathdoes not raise. nginx finishes the request, writes its access log line and closes the connection. When the handler that calledNginx.redirectends with the 500 that/pathleft in the status, nginx tries to send that status again and logsheader already sentat levelalert, as it already does when a handler returns a status after the response of the redirect target. Theerror.loglines 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
/pathruns in a fiber of its own inside the Ruby call of the handler that calledNginx.redirect, so the exception jumped over the nginx functions between the two (ngx_http_internal_redirectand 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
/pathlets 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_ said that the other location returns the response to the client, and said nothing about an exception in it.method/README.md
- The defect: mruby passes the exception that ends a fiber resumed from C
by longjmp to the innermost running mruby VM. The handler of
- Affected: code that calls
Nginx.redirectwith an internal location whose mruby handlers or filters raise, and code that rescued such an exception aroundNginx.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.redirectto react to a failure of the redirect target moves therescueinto the code of that location. - Not changed by this release: when the handler that called
Nginx.redirectanswers the request, with a status or an exception, while a rewrite handler of the redirect target still sleeps inNginx::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 leftNginx::Stream::Connection.stream_statusatNginx::Stream::DECLINED, the default, let the session go on. When the code raised, or set the status toNginx::Stream::ABORT,Nginx::Stream::OKor 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 thestream {}block share, and the next session that the worker handled, in any server of the block, was aborted before its code ran;error.loglogged 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::DECLINEDgoes on with the next handler,Nginx::Stream::OKwith the next phase,Nginx::Stream::DONEandNginx::Stream::AGAINleave the session waiting, and an exception,Nginx::Stream::ABORTor 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_DONEto it, and the timer that resumes the code dropped the result of the code after the sleep, except forNGX_DECLINED. After an exception it did not clear the exception from the shared mruby state either, as the code without the sleep does, andngx_stream_mrb_start_fiberof 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_ documentedmethod/README.md Nginx::Stream::Async.sleepfor the access phase andNginx::Stream::Connection.stream_status=withNginx::Stream::ABORTto deny a client, and said nothing about the result after the sleep.
- The defect: nginx’s checker of the access phase handles the result of a
handler that returns at once. A handler that sleeps returns
- Affected:
mruby_streamandmruby_stream_codecode that callsNginx::Stream::Async.sleepand then raises, or setsNginx::Stream::Connection.stream_statusto a value other thanNginx::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::ABORTafter the sleep to deny a client: the session now ends. Code that setNginx::Stream::OKafter the sleep now goes on with the next phase, for exampleproxy_pass; setNginx::Stream::ABORTinstead 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.newandNginx::Stream::Connection#upstream_server=read their argument as a String without checking its class. Withnil, 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 withRSTRING_LENandRSTRING_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.rbandtest/t/cases/stream_upstream_server_checks.rb, passnil. 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_ described the argument as a string (method/README.md Nginx::Upstream#new(string),Nginx::Upstream#server=(string)) and said nothing about other classes.
- The defect: the methods took the argument with
- Affected: code that passes anything but a String to these methods, for
example
nilfrom 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")foundupstream backend, andNginx::Upstream.new("backend")foundupstream backend_awhen 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_strncasecmpover the length of the argument, inngx_mrb_upstream_initandngx_stream_mrb_connection_init. It was found when the documentation of #648 was checked against the code, andtest/t/cases/filter_connection.rbpinned 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.
- The defect: the comparison was
- 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 azone, 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 theNginx::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 withworker_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.mdonnext). - 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
resolveparameter (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_ said that the setters do not check for a zone.method/README.md
- 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
- 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
zonefrom an upstream block that a script changes with these setters; their change applies to one worker either way.
New features
Section titled “New features”None.
Nginx::Stream::Async.sleepno 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_sleeparmed the timer (ctx->sleep, anngx_event_tin 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_fibernow registers a connection pool cleanup that cancels a pending sleep timer and unregisters the fiber when the session ends, as the HTTPNginx::Async.sleeppath already did, and the fiber is registered with the GC once per fiber instead of once perNginx::Stream::Async.sleepcall (a fiber that slept more than once had stayed on the GC root). The change is insrc/streamonly; no directive, Ruby method or build option changed (#645).
Build and test changes
Section titled “Build and test changes”-
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.rbruns whenNGX_MRUBY_TEST_AUTO_SSLnames the directory thatsh test/auto_ssl/deps.sh start DIRprepared, and CI runs it in every cell of thebuildmatrix. 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, whichbuild.shandtest.shread, 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 thatbuild.shdownloads by default has these fixes; a build that names its own nginx source withNGINX_SRC_ENVuses 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 theworking_directorydirective 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 workflowsite-v2.ymlbuilds the site ofnextwith the pull request’sdocs/under/v2/and fails on a broken link of the 2.x pages; on every push tomaster, the new workflowsite-dispatch.ymlstarts the site’s workflow onnext. Neither is in theneedsofci-ok(“The 2.x pages of the site” in docs/test/README.md) (#656).
Upgrading
Section titled “Upgrading”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 ofdehydrated --croninerror.log, set the level of theerror_logthat the server uses toinfo. Code that relied on the method retrying the registration within one call calls it again in a later handshake (entry 1).Nginx::Async.sleepin 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 returnsNginx::HTTP_CLOSE(444) withNginx.returninstead. 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.redirectruns: nothing for code that did not rescue it. Code that rescued it aroundNginx.redirectmoves therescueinto the code of that location (entry 3). - The result of a stream session after
Nginx::Stream::Async.sleep: nothing for code that setsNginx::Stream::ABORTto deny a client. Code that setNginx::Stream::OKafter the sleep setsNginx::Stream::ABORTinstead where the session must not go on (entry 4). Nginx::UpstreamandNginx::Stream::Connectionwith an argument that is not a String: pass a String, for example withto_s, and handle a missing value before the call (entry 5).Nginx::Upstream.newandNginx::Stream::Connection.newwith a part of a name: pass the whole name of theupstream {}block (entry 6).- The upstream server setters with a
zone: removezonefrom an upstream block that a script changes with them (entry 7).