Skip to content

ngx_mruby v3.0.0

The bundled mruby is 4.0.0 instead of 3.3.0 (#561)

Section titled “The bundled mruby is 4.0.0 instead of 3.3.0 (#561)”
  • Before: ngx_mruby 2.x bundles mruby 3.3.0 and builds the core gems that full-core.gembox of mruby 3.3.0 selects, mruby-print included.
  • Now: this release bundles mruby 4.0.0, the tag 4.0.0 of mruby (commit 831da26b9021, 2026-04-20). The default build_config.rb takes the core gems that NGX_MRUBY_CORE_GEMS names: the set that full-core.gembox of mruby 3.3.0 selected, without mruby-print (mruby 3.4 removed it; Kernel#print, #puts and #p come from mruby-io), and with hal-posix-io, hal-posix-dir and hal-posix-socket, the POSIX back ends that mruby-io, mruby-dir and mruby-socket of 4.0.0 need, which add no class or method. It leaves out mruby-task (with its back end hal-posix-task), mruby-encoding, mruby-strftime (Time#strftime) and mruby-benchmark (Benchmark), which full-core.gembox of 4.0.0 takes, and keeps mruby-sleep, which full-core.gembox of 4.0.0 leaves out. The line that nginx logs at the info level when it reads the configuration ends in (mruby/4.0.0) mechanism enabled. The entries below list the differences that scripts, log monitoring, C extensions and builds see, as far as they were found and measured for ngx_mruby: they are not a complete list of the changes between mruby 3.3.0 and 4.0.0. mruby lists its user-visible changes in doc/mruby3.4.md and doc/mruby4.0.md.
  • Affected: every build of the module, and every Ruby script that it runs.
  • What to do: rebuild the module together with the bundled mruby, then check the entries below and, for the changes that they do not cover, mruby’s two lists; Migrating from ngx_mruby 2.x to 3.0.0 has the steps. Why the bundled mruby changes: the v3 plan puts stability, performance and current dependencies before new capabilities, and mruby publishes no maintenance releases of 3.x (docs/proposals/v3-plan.md, sections 3.3 and 6). Decision 1 of the plan (section 7) sets mruby 4.1.0 as the target of v3; the owner approved 4.0.0 as a step before it on 2026-10-04, and next moves to 4.1.0 when 4.1.0 is tagged.

A build_config.rb of your own needs the core gem changes of mruby 4.0.0 (#561)

Section titled “A build_config.rb of your own needs the core gem changes of mruby 4.0.0 (#561)”
  • Before: a build_config.rb of your own could name mruby-print, or take full-core.gembox and get the core gems of mruby 3.3.0, mruby-sleep included.
  • Now: mruby 4.0.0 has no mruby-print, and ngx_mruby_core_gems in build_config.rb stops the build on a name that the bundled mruby does not have. full-core.gembox of 4.0.0 takes mruby-task, which redefines Kernel#sleep, arms a SIGALRM interval timer (setitimer) in every process that opens an mrb_state and allows at most 8 mrb_states (MRB_TASK_MAX_VMS), and mruby-encoding (see the entry of String#valid_encoding?), and leaves out mruby-sleep. mruby-io, mruby-dir and mruby-socket of 4.0.0 need a HAL back end; without one in the list they load hal-posix-io, hal-posix-dir and hal-posix-socket by themselves with the warning “No HAL specified … (explicit selection recommended)”. A 4.0.0 build whose work directory was mounted at /s in a container chose the Windows back ends by itself and stopped at windows.h: No such file or directory, because for_windows? in mruby/lib/mruby/gem.rb treats a one-letter directory at the root as a Windows drive.
  • Affected: builds with a build_config.rb other than the one of this release that name mruby-print or take full-core.gembox.
  • What to do: start from the build_config.rb of this release, or select the core gems by name as NGX_MRUBY_CORE_GEMS does: remove mruby-print, add hal-posix-io, hal-posix-dir and hal-posix-socket, and decide on mruby-task and mruby-encoding as Core gems describes.

The third-party mrbgems are built at the commits that build_config.rb.lock records (#558)

Section titled “The third-party mrbgems are built at the commits that build_config.rb.lock records (#558)”
  • Before: build_config.rb names the third-party mrbgems without a commit, and .gitignore lists the build_config.rb.lock that mruby’s rake writes, so the lock stays out of the repository. A build from a new checkout or a source archive clones the head of each gem’s branch with git clone --depth 1, so two builds of the same ngx_mruby commit made on different days can get different gem code.
  • Now: build_config.rb.lock is committed. It records a commit for each third-party gem of the default build and for each gem that those depend on, for example ksss/mruby-stringio, which the bundled rack-based-api needs. A build from a new checkout or a source archive clones each of them with its whole history and checks the recorded commit out, so every build of an ngx_mruby commit gets the same gem code. A gem that a build_config.rb of your own adds is cloned at the head of its branch, and rake adds its entry to the lock in your tree, which git status then shows as modified. rake does not fetch into a clone that exists: after a change of the lock, a build in a tree that keeps mruby/build stops with fatal: reference is not a tree when a clone does not have the new commit. symisc/vedis, which matsumotory/mruby-vedis clones by itself, is not pinned.
  • Why it changed:
    • The reason: the v3 plan pins every third-party gem by commit and commits build_config.rb.lock, so that the same tag builds the same binary (docs/proposals/v3-plan.md, section 4, Pillar A).
    • Why the old behavior could not be kept: without the pins, the gem code of a build depends on the day of the build. A change in any of the gem repositories reaches every later build of a released tag, and every CI run, without a change in ngx_mruby.
    • What the documentation promised: the documentation of 2.x said nothing about the commits of the third-party gems. docs/install/README.md lists the default mrbgems by name only.
  • Affected: builds from source that expect the newest commits of the third-party gems, build_config.rb files of your own that add gems, and trees that keep mruby/build when the lock changes.
  • What to do: nothing for a new checkout or a source archive. To build a gem at another commit, change its entry in the lock as Gem commits describes. After a change of the lock, for example one that a pull brings in, remove mruby/build before the next build.

Hash#inspect and Hash#to_s use a new format (#561)

Section titled “Hash#inspect and Hash#to_s use a new format (#561)”
  • Before: {"a"=>1} and {:a=>1}.
  • Now: {"a" => 1} and {a: 1} (since mruby 3.4). Everything that prints a Hash with these methods changes with them: string interpolation ("#{h}"), sprintf("%s", h) and Array#inspect.
  • Affected: scripts that put the inspect or to_s of a Hash into a response body or a header, or compare it with a fixed string. The test suite of ngx_mruby saw the change (/async_http_sub_request in test/t/ngx_mruby.rb).
  • What to do: do not use the inspect or to_s of a Hash as a data format, in a response body or in a header. Build the string yourself, or use JSON.generate.

Exception#inspect, and so the mrb_run failed lines of error.log, use a new format (#561)

Section titled “Exception#inspect, and so the mrb_run failed lines of error.log, use a new format (#561)”
  • Before: boom (RuntimeError). ngx_mruby writes the exception of a script with Exception#inspect into the mrb_run failed lines of error.log (level error; the line that an exception in mruby_stream_server_context_code logs while nginx reads the configuration, mrb_run failed: ngx_mruby stream module configuration failed: error: ..., is at level emerg), for example mrb_run failed: return 500 HTTP status code to client: error: boom (RuntimeError).
  • Now: #<RuntimeError: boom>, so the line ends in error: #<RuntimeError: boom>. Exception#message and #to_s are unchanged.
  • Affected: log parsers and alerts that match the old form, and scripts that use Exception#inspect. The test suite of ngx_mruby saw the change in error.log.
  • What to do: update log parsers and alerts that match the old form.

The messages logged while nginx reads the configuration are at the info level instead of notice (#565)

Section titled “The messages logged while nginx reads the configuration are at the info level instead of notice (#565)”
  • Before: while nginx reads the configuration, ngx_mruby logs these messages at the notice level: ngx_mruby/<version> (mruby/<version>) mechanism enabled, once for the http {} block; ngx_mruby NOTICE ...: compile info: ... for each directive with Ruby code, and ngx_mruby-stream-module NOTICE ...: compile info: ... for each one in stream {}; and ngx_mruby NOTICE ...: target variable=(...) for each mruby_set and mruby_set_code. nginx writes what it logs while it reads the configuration to its startup log, at the notice level whatever error_log says, so every start of nginx writes these lines, also with error_log ... warn;.
  • Now: the same messages are logged at the info level, and their text says INFO instead of NOTICE (ngx_mruby INFO ..., ngx_mruby-stream-module INFO ...). A start of nginx writes none of them. nginx -t writes them at [info] to the startup log only: nginx copies a line of the startup log to the terminal only at warn and above, so the output of nginx -t does not show them. A reload writes them to the error_log of the running configuration when that error_log is at info or debug. Errors while nginx reads the configuration, such as a script file that cannot be opened, are still logged at emerg.
  • Why it changed:
    • The defect: every start of nginx wrote these lines, and an error_log at a level above notice did not stop them (#320).
    • Why the old behavior could not be kept: nginx writes what it logs while it reads the configuration to its startup log at the notice level, before the configured error_log takes over, so no error_log setting filters a message at notice. Only a level below notice keeps the lines out of a start.
    • What the documentation promised: the documentation of 2.x said nothing about these messages.
  • Affected: log monitoring and scripts that look for these lines when nginx starts, for example to see that the module is loaded.
  • What to do: to see them, run nginx -t -e stderr, which writes the startup log to the terminal; read the startup log after nginx -t; or reload with error_log at info. The startup log is the file given with -e, or else the error log path compiled into nginx (logs/error.log under the prefix by default), as Messages logged while nginx reads the configuration describes.

The message of NoMethodError names the class of the receiver (#561)

Section titled “The message of NoMethodError names the class of the receiver (#561)”
  • Before: undefined method 'foo'.
  • Now: undefined method 'foo' for NilClass.
  • Affected: scripts and log monitoring that match the message of a NoMethodError.
  • What to do: match on the exception class, not on the message.
  • Before: not enforced: a method defined after private or protected, initialize, a method defined with def at the top level, and the Kernel methods could be called with an explicit receiver, Kernel itself included (Kernel.Integer("12") is 12). Module#instance_methods and #methods of an object list private methods too: for a class with a method pub and a private method priv, instance_methods(false) is [:priv, :pub].
  • Now: enforced (since mruby 3.4): these methods raise NoMethodError (private method '<name>' called for <class>) when called with an explicit receiver other than self. This includes the Kernel methods such as raise, sprintf, puts, Integer and rand, and initialize_copy, respond_to_missing? and method_missing, which mruby 4.0.0 defines as private ("a".initialize_copy("b") raises). Called on Kernel itself, only Kernel.format, Kernel.sprintf, Kernel.raise, Kernel.sleep and Kernel.usleep still work; the other private methods of Kernel, among them Integer, Float, String, Array, Hash, eval, rand and puts, raise NoMethodError (Kernel.Integer("12") raises private method 'Integer' called for Module). respond_to? returns true for a private method on both versions (Kernel.respond_to?(:Integer) is true), so it does not tell whether such a call works. Module#instance_methods and #methods of an object leave private methods out (instance_methods(false) of the class above is [:pub]); Module#private_instance_methods and #private_methods list them. In handler code, a method defined with def at the top level stays public, so a call of it with an explicit receiver still works (the proposal for #502, section 1.4).
  • Affected: scripts that call such a method with an explicit receiver, Kernel.Integer(x) and the other calls on Kernel included, and scripts that look for private methods in the result of instance_methods or methods.
  • What to do: call them without a receiver (Integer(x) instead of Kernel.Integer(x)), or with send (Kernel.send(:Integer, x) works on both versions). To list private methods, use private_instance_methods or private_methods.

Module#remove_const, #private, #public, #protected, #module_function and the hooks are private (#561)

Section titled “Module#remove_const, #private, #public, #protected, #module_function and the hooks are private (#561)”
  • Before: public: Foo.remove_const(:X), M.included(Foo), M.prepended(Foo) and obj.singleton_method_added(:m) work. The hooks are #included, #extended, #prepended, #inherited and #method_added of Module and Class, and BasicObject#singleton_method_added.
  • Now: private: called with an explicit receiver, they raise NoMethodError: private method ... called. send works, and mruby still calls the hooks itself: prepend calls prepended, and def obj.m calls singleton_method_added.
  • Affected: scripts that call one of these methods with an explicit receiver.
  • What to do: call them in the body of the class or module without a receiver, or with send (Foo.send(:remove_const, :X)).

Overrides of append_features, prepend_features and extend_object are not called (#561)

Section titled “Overrides of append_features, prepend_features and extend_object are not called (#561)”
  • Before: include, prepend and extend call these three methods, so a module can override them.
  • Now: the three methods are gone (Module.new.respond_to?(:append_features, true) is false, and the same for the other two). include, prepend and extend add the module without calling them, so an override is skipped without an error. The hooks included, prepended and extended are still called.
  • Affected: modules that override one of the three methods.
  • What to do: move the code to included, prepended or extended.

allbits?, anybits? and nobits? are methods of Integer only (#561)

Section titled “allbits?, anybits? and nobits? are methods of Integer only (#561)”
  • Before: methods of Numeric, so they work on a Float (1.5.allbits?(1) is true).
  • Now: methods of Integer only; on a Float they raise NoMethodError. On an Integer they work as before (6.allbits?(2) is true).
  • Affected: scripts that call them on a Float.
  • What to do: convert a Float with to_i first.
  • Before: FileTest is a class.
  • Now: FileTest is a module. FileTest.exist? and its other methods are called as before.
  • Affected: scripts that use FileTest as a class, other than by calling its methods.
  • What to do: nothing for calls of its methods.

Assigning a constant in a method body is a SyntaxError (#561)

Section titled “Assigning a constant in a method body is a SyntaxError (#561)”
  • Before: allowed (def m; X = 1; end).
  • Now: SyntaxError: dynamic constant assignment, when the script is compiled.
  • Affected: scripts that assign a constant inside a method body. The Rack-style Kernel#run of ngx_mruby sets Server with Kernel.const_set.
  • What to do: use Object.const_set(:X, 1) (or Kernel.const_set).

Float values have no bit operations (#561)

Section titled “Float values have no bit operations (#561)”
  • Before: ~, &, | and ^ work on the integer part (1.5 & 1 is 1, ~1.5 is -2). For a shift count from 1 up, << multiplies the Float by that power of two and truncates (1.5 << 1 is 3, -1.5 << 1 is -3, 1.5 << 40 is 1649267441664), and gives a Float when the result is outside the Integer range (1.5 << 70 is 1.77088743107612e+21). For a shift count from 1 to 32, >> divides a positive Float by that power of two and truncates (8.0 >> 1 is 4, 1.5 >> 1 is 0), and returns -1 for every negative Float (-8.0 >> 1 is -1). For a count above 32, >> returns 0 for a positive and -1 for a negative Float (1e20 >> 32 is 23283064365, 1e20 >> 33 is 0). A count of 0 returns the Float unchanged (-1.5 >> 0 is -1.5), and a negative count shifts the other way (1.5 >> -1 is 3).
  • Now: ~, &, |, ^, >> and << raise NoMethodError on a Float (mruby 3.4 removed all six).
  • Affected: scripts that apply one of the six operators to a Float.
  • What to do: for ~, &, | and ^, convert with to_i first. For << and >>, to_i first can give another number (1.5.to_i << 1 is 2, -8.0.to_i >> 1 is -4). Where the old number matters: for << by a count from 1 up, multiply the Float by the power of two and call to_i; for >> by a count from 1 to 32, divide a positive Float by the power of two and call to_i, and use -1 for a negative Float; for >> by a count above 32, use 0 for a positive and -1 for a negative Float (dividing would give another number: (1e20 / 2**40).to_i is 90949470, where 1e20 >> 40 was 0); for a count of 0, keep the Float.

String#valid_encoding? is not in the default build (#561)

Section titled “String#valid_encoding? is not in the default build (#561)”
  • Before: defined by mruby-string-ext. With the default build_config.rb, ngx_mruby 2.x builds mruby without MRB_UTF8_STRING, and the method returns true for every string, even "\xfe". A build whose build_config.rb defines MRB_UTF8_STRING gets false for a string that is not valid UTF-8.
  • Now: since mruby 3.4 only mruby-encoding defines the method, and the default build leaves that gem out, so a call raises NoMethodError (undefined method 'valid_encoding?' for String) and a handler that calls it answers 500.
  • Affected: scripts that call String#valid_encoding?.
  • What to do: with the default build_config.rb of 2.x, drop the call (it never returned false there), or build with mruby-encoding and check the scripts as Core gems says. A 2.x build that defined MRB_UTF8_STRING needs mruby-encoding to keep the check. Why the default build leaves mruby-encoding out: the gem defines MRB_UTF8_STRING for the whole build, so String#size, #[] and #index count characters instead of bytes, and a script that computes a Content-Length from String#size would get another number; the owner decided on 2026-10-04 to leave it out of the default build and keep it opt-in.

A seeded Random gives other numbers (#561)

Section titled “A seeded Random gives other numbers (#561)”
  • Before: Random.new(seed) and srand(seed) give one sequence for a seed (xoshiro).
  • Now: another sequence for the same seed: mruby 4.0 uses PCG instead of xoshiro.
  • Affected: scripts that depend on the numbers of a seeded generator.
  • What to do: do not depend on the numbers of a seeded generator across versions.

Set#inspect and Set#to_s use a new format, and Set#flatten_merge is removed (#561)

Section titled “Set#inspect and Set#to_s use a new format, and Set#flatten_merge is removed (#561)”
  • Before: #<Set: {1, 2}>. Set#flatten_merge is a public method: Set.new([1]).flatten_merge(Set.new([2, Set.new([3])])) is the Set of 1, 2 and 3.
  • Now: Set[1, 2]. Everything that prints a Set with these methods changes with them: string interpolation ("#{set}"), sprintf("%s", set) and Array#inspect. mruby 4.0.0 removed Set#flatten_merge, and a call raises NoMethodError; mruby’s lists of user-visible changes do not name it.
  • Affected: scripts that use the inspect or to_s of a Set as a data format, or compare it with a fixed string, and scripts that call Set#flatten_merge.
  • What to do: as for Hash#inspect: build the string yourself (set.to_a.join(",") gives the same string on both versions). In place of s.flatten_merge(t), call s.merge(Set.new(t).flatten), which gives on both versions the Set that s.flatten_merge(t) gave on 2.x, for a Set and for an Array t.

Hash#default_proc= raises TypeError for an object that is not a Proc (#561)

Section titled “Hash#default_proc= raises TypeError for an object that is not a Proc (#561)”
  • Before: accepted.
  • Now: TypeError.
  • Affected: scripts that pass Hash#default_proc= an object that is not a Proc.
  • What to do: pass a Proc or nil.

yield with keyword arguments to a block with keyword parameters works (#561)

Section titled “yield with keyword arguments to a block with keyword parameters works (#561)”
  • Before: ArgumentError: missing keyword.
  • Now: the block gets the keyword arguments.
  • Affected: scripts that yield keyword arguments to a block that takes keyword parameters, for example a script that rescued the ArgumentError.
  • What to do: nothing; it is a bug fix of mruby.

A return inside a block or a for body of handler code ends the code (#561)

Section titled “A return inside a block or a for body of handler code ends the code (#561)”
  • Before: a return inside a block, such as [0].each do ... end or 3.times { ... }, or inside the body of for, at the top level of handler code raised LocalJumpError, and the handler answered 500 with mrb_run failed: return 500 HTTP status code to client: error: unexpected return (LocalJumpError) in error.log. A content handler that had written output before it answered 500 as well and did not send that output. 2.7.0 sent that output with 200 and only logged the exception; 2.7.1 changed this for every exception after Nginx.rputs (entry 3 of the 2.7.1 release notes). The variable of mruby_set_code got an empty value. A return at the top level of the code, outside a block, ended the code.
  • Now: such a return ends the code, as a return at the top level does. The code after it does not run, and the response is the one that the code set before it: the status that Nginx.return set and the output that Nginx.rputs wrote. In mruby_set_code, the value of a return in a block becomes the variable. error.log has no line for it.
  • Why it changed:
    • The defect: #502: the return that ends a handler early raised LocalJumpError when it was written inside a block or a for body, and the handler answered 500 instead of the response that the code had set.
    • Why the old behavior could not be kept: the error came from mruby 3.3.0. ngx_mruby runs handler code in the first frame of a fiber; mruby 3.3.0 refuses that frame as the frame that such a return leaves, and mruby 4.0.0 accepts it (section 1.3 of the proposal for the frame of handler code). Keeping the error would have meant changing the bundled mruby 4.0.0 back to the rule of 3.3.0, for a behavior that #502 reports as a defect.
    • What the documentation promised: docs/class_and_method/README.md of 2.x says that ngx_mruby supports the return statement in handler code and that it returns from the handler, with no exception for a block.
  • Affected: handler code with a return inside a block or the body of for at its top level; a return inside a method or a lambda is not affected. Log alerts and scripts that relied on the 500 or on the LocalJumpError line are affected as well.
  • What to do: nothing. The two ways around it on 2.x, a return at the top level of the code and the code in a method or a lambda, work on both versions.
  • Not changed by this release: the frame in which handler code runs. self is main, local variables belong to one run of the code, and constants, globals, instance variables of main and methods defined with def persist in the worker (section 1.4 of the proposal).

A content handler that ends without output answers 200 with an empty body instead of 500 (#574)

Section titled “A content handler that ends without output answers 200 with an empty body instead of 500 (#574)”
  • Before: on 2.7.1 and 2.7.2, a content handler on the main request that ended without output (it wrote nothing, or only "" or nil with Nginx.rputs, or only set response headers) got 500 with nginx’s error page (or the error_page configured for 500) and Connection: close when it set no status, Nginx::HTTP_OK (with Nginx.return, Nginx.send_header or Nginx.status_code=) or another status below 300 other than 201 and 204 (for example Nginx::HTTP_ACCEPTED), also after Nginx::Async::HTTP.sub_request resumed it (entry 13 of the 2.7.1 release notes). error.log got a line at the error level: status code is 200, but response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERROR, status code is not set, and response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERROR or status code is <N>, but response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERROR. On 2.7.0 and earlier, Nginx::HTTP_OK without output got the same 500, and the other cases got no response at all: nginx closed an HTTP/1.0 connection without sending a byte and kept a keep-alive connection open without a response until keepalive_timeout.
  • Now: such a handler answers as nginx’s own return 200; does: with the status it set, 200 when it set none, Content-Length: 0 and an empty body. It sends no Content-Type, as ngx_mruby’s answers with output do not; nginx’s return 200; sends the one of default_type. A HEAD request gets the same header. The header filters and body filters of the location (mruby_output_header_filter, mruby_output_body_filter and their _code forms) run on the empty response. The empty answer discards an unread request body first, as the answer with output does since 2.7.1 (entry 12 of the 2.7.1 release notes), so the next request on a kept connection is the one that the client sent after the body. nginx keeps a keep-alive connection for the next request, with one exception: after a HEAD request to a location with an mruby output body filter, nginx sends the header and closes the connection, as it does there for a handler with output on 2.7.1 and on this release. error.log gets none of the lines above for it.
  • Why it changed:
    • The defect: #200: a content handler that ended with Nginx.return Nginx::HTTP_OK and no body answered 500, where nginx’s own return 200; answers 200 with an empty body. HTTP allows that response: “a 200 response is expected to contain message content unless the message framing explicitly indicates that the content has zero length” (RFC 9110, section 15.3.1), which Content-Length: 0 does. 2.7.1 gave the same 500 to no status and to the other statuses below 300 that nginx does not answer by itself, because those requests had got no response at all (#566).
    • Why the old behavior could not be kept: with the 500, a content handler cannot answer 200 with an empty body at all; the only empty answer that it can choose is 204, which is another status for clients and caches. The v3 plan accepts the empty 200 as a behavior change of v3 (section 7, item 8 of docs/proposals/v3-plan.md); the 2.x line keeps the 500 because it puts compatibility first.
    • What the documentation promised: the documentation of 2.7.1 described the old behavior: a content handler that ends without output answers 500, and an empty body takes Nginx::HTTP_NO_CONTENT (Nginx.return of 2.7.1). Before 2.7.1, it said only that Nginx.return “Returns nginx status code to send back HTTP status to the client” (Nginx.return of 2.7.0).
  • Affected: mruby_content_handler and mruby_content_handler_code handlers that can end without output, for example a handler that writes only when a condition holds, one that passes a value that can be empty or nil to Nginx.rputs, one that only sets response headers, and one that ends with Nginx.return Nginx::HTTP_OK and no body, and log monitoring of the lines above. Unchanged: a status below 200 without output (500 and the status code is <N>, but response body is empty line, as on 2.7.1 and 2.7.2), 201, 204 and the statuses from 300 up (nginx answers them), handlers with output, Nginx.redirect, the other handlers (Nginx.return Nginx::HTTP_OK without output in a rewrite or access handler still answers 500 and logs status code is 200, but response body is empty), and subrequests (Nginx::Async::HTTP.sub_request, SSI includes and auth_request): the content handler of a subrequest that returns Nginx::HTTP_OK without output still sets the subrequest’s status to 500, and one that writes nothing and sets no status still ends the subrequest without a response body and without a status.
  • What to do: nothing for most configurations: a handler that should answer with an empty body may now return Nginx::HTTP_OK. A script that relied on the 500 to signal that it had nothing to send must set that status itself, for example Nginx.return Nginx::HTTP_INTERNAL_SERVER_ERROR or Nginx.return Nginx::HTTP_NOT_FOUND, which nginx answers as before. Log monitoring that matches response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERROR finds only the statuses below 200 of content handlers, and Nginx::HTTP_OK in the other handlers and in subrequests.
  • Tests: port 18136 (test/conf/conf.d/54-content-empty-body.conf, test/t/cases/content_empty_body.rb, with two POST requests whose body the handler does not read on one connection), and the cases “Nginx.return 200 dont have body” of test/t/ngx_mruby.rb and “send_header 200 without a body answers 200 with an empty body” of test/t/cases/request_api.rb.

An output body filter that ends with an empty body sends the response instead of closing the connection (#574)

Section titled “An output body filter that ends with an empty body sends the response instead of closing the connection (#574)”
  • Before: when an output body filter (mruby_output_body_filter, mruby_output_body_filter_code) ended with an empty body, because it set "" with Nginx::Filter#body= or Nginx::Filter#output, or because the response had no body and the filter left it empty, nginx logged zero size buf in writer at the alert level and closed the connection before it wrote the status line: the client received no byte. This holds on 2.7.2, 2.7.1 and earlier releases.
  • Now: the response is sent with its status, Content-Length: 0 and an empty body, and error.log gets no alert line for it.
  • Why it changed:
    • The defect: the body filter passed on the empty body as a buffer of zero bytes marked as being in memory, which nginx’s writer rejects (zero size buf in writer), so the response was never sent. nginx’s own return 200; passes an empty last buffer that is not marked so (ngx_http_send_response). The 2.x line has this defect too: test/t/cases/filter_connection.rb of 2.7.1 records it as the behavior of 2.x (“an empty String closes the connection without a response”).
    • Why the old behavior could not be kept: a response that is never sent leaves the client without an answer. The empty 200 of the entry above also passes through the body filter of its location, so without this change such a location would send no response at all instead of the empty 200.
    • What the documentation promised: Nginx::Filter#body= of 2.7.1 says that the response is sent with exactly the body that the filter sets, and that Content-Length is its length, with no exception for an empty String.
  • Affected: locations with an output body filter that can end with an empty body, including a location whose content handler ends without output (the entry above), and log monitoring of the zero size buf in writer line, which these responses no longer log.
  • What to do: nothing.
  • Tests: port 18112 (test/t/cases/filter_connection.rb, “an empty String sends the status with Content-Length: 0”) and the locations with a body filter on port 18136 (test/t/cases/content_empty_body.rb).

The accessors of Nginx::Var become methods of Nginx::Var at their first use in a worker (#610)

Section titled “The accessors of Nginx::Var become methods of Nginx::Var at their first use in a worker (#610)”
  • Before: every v.NAME and v.NAME = value on an Nginx::Var went through Nginx::Var#method_missing. Nginx::Var had three methods of its own, method_missing, exist? and set: v.respond_to?(:request_method) returned false, and Nginx::Var.instance_methods(false) listed only those three. When a script defined a method on Object or Kernel whose name was also the name of a variable, for example with a def at the top level of handler code, v.NAME called that method instead of reading the variable.
  • Now: the first v.NAME or v.NAME = value of a name in a worker process also defines NAME or NAME= as a method of Nginx::Var, and the later accesses of the name in that worker call the method. The method reads and writes the variable as method_missing does: the values, nil for a name that is not a variable, the exceptions and the lines of error.log are the same. A worker defines a getter and a setter for each declared variable that scripts use (the variables of nginx and its modules, and the ones that set declares), and a getter for the first 16 names of prefix variables that scripts use, such as arg_q or http_x_request_id. A name gets a method only when it is made of a to z, 0 to 9 and _, with = at the end for a setter; v.Request_Method keeps reading $request_method through method_missing. A name that is not a variable, a setter of a prefix variable, an instance of a subclass of Nginx::Var, a frozen Nginx::Var, and a name that Nginx::Var or one of its ancestors already has as a method get no method. After the first access, v.respond_to?(:NAME), v.methods, Nginx::Var.instance_methods(false) and Nginx::Var.method_defined?(:NAME) show the method. What they show depends on the requests that the worker has served, so two workers can answer differently. Once the method exists, a method of the same name that a script defines later on Object or Kernel no longer hides the variable from v.NAME.
  • Why it changed:
    • The reason: with mruby 4.0.0, the lookup of a method name that no class defines compares the name with every method of Nginx::Var, Object, Kernel and BasicObject, more than 110 entries, and mruby does not cache a name that it did not find. Each v.NAME paid that search before method_missing ran. In the callgrind comparison of #561, the search made 74% of the difference of the var scenario and 59% of that of the auth scenario against mruby 3.3.0 (section 1.1 of the proposal). A method of the name is found in mruby’s method cache instead.
    • Why the old behavior could not be kept: mruby’s method cache answers only for a method that the lookup found, so a change that lets v.NAME skip the search has to define a method, and introspection shows it. The other ways of the proposal leave the scripts as they are written today on the slow path, or wait for a release of mruby (section 2 of the proposal). The owner accepted the change to introspection as a behavior change of 3.0.0 on 2026-10-15, and confirmed it on 2026-10-19 (the status of the proposal).
    • What the documentation promised: the Nginx::Var class of 2.7.1 says that Nginx::Var#method_missing is called when Nginx::Var#VARNAME or Nginx::Var#VARNAME= is evaluated. It said nothing about respond_to? or the methods of Nginx::Var.
  • Affected: scripts that call respond_to?, methods, instance_methods or method_defined? on Nginx::Var or its instances, for example to tell whether a name is a variable, scripts that define a method on Object or Kernel with the name of a variable that they also read with v.NAME, and scripts that wrap Nginx::Var#method_missing, for example with a module prepended to Nginx::Var whose method_missing calls super: the wrapper runs only for the first access of a name that gets a method.
  • What to do: use v.exist?(name) to tell whether a variable exists. Give the methods of your scripts names that are not names of nginx variables.
  • Tests: port 18159 (test/conf/conf.d/68-var-accessor.conf, test/t/cases/var_method_lookup.rb).

Nginx::Connection, Nginx::Server and Nginx::Request read their variables without calling Nginx::Var (#610)

Section titled “Nginx::Connection, Nginx::Server and Nginx::Request read their variables without calling Nginx::Var (#610)”
  • Before: Nginx::Connection#remote_ip, #remote_port, #local_ip and #local_port, Nginx::Server#document_root, #realpath_root and #path, and Nginx::Request#hostname, #authority, #filename, #user and #get_body called the accessor of their variable (remote_addr, remote_port, server_addr, server_port, document_root, realpath_root, host, http_host, request_filename, remote_user and request_body) on an Nginx::Var that the object created and kept in its instance variable @iv_var. A script that defined such a method on Nginx::Var, defined a method_missing of its own there, or put another object in @iv_var, changed what these methods returned.
  • Now: these methods read the variable from nginx in C. They return the value that nginx has for the variable, whatever methods a script defines on Nginx::Var, and they neither create an Nginx::Var nor set @iv_var. Nginx::Request#var still creates the Nginx::Var of the object and keeps it in @iv_var. Outside a request they raise the same RuntimeError as before.
  • Why it changed:
    • The reason: the names of these variables are fixed, and “Design principles” in AGENTS.md asks that nothing fixed is looked up or allocated per request when it can be prepared once at initialization. The Ruby call looked the method up on Nginx::Var on each call, which takes the search of the entry above, and each object created an Nginx::Var and looked up its class. The names and their hash keys are now prepared once (candidate (d) of the proposal).
    • Why the old behavior could not be kept: the Ruby call on the object in @iv_var is what let a script change the values. Reading the variable without that call removes both the cost and that effect.
    • What the documentation promised: the Nginx::Connection class of 2.7.1 says that remote_ip, remote_port, local_ip and local_port are the same as the nginx variables $remote_addr, $remote_port, $server_addr and $server_port, and Nginx::Server#realpath_root says the same of $realpath_root. That is what they return now in every case.
  • Affected: scripts that change what these methods return by defining methods on Nginx::Var, a method_missing of their own on it, or another object in @iv_var, and scripts that read @iv_var of these objects without calling Nginx::Request#var first.
  • What to do: to return another value, define the method on Nginx::Connection, Nginx::Server or Nginx::Request itself, or call a method of your own. Read variables with Nginx::Var (Nginx::Request#var, Nginx.var or Nginx::Var.new) instead of @iv_var.
  • Tests: port 18159 (test/t/cases/var_method_lookup.rb).

Dir comes from mruby’s core gem, and a closed Dir raises IOError instead of RuntimeError (#559)

Section titled “Dir comes from mruby’s core gem, and a closed Dir raises IOError instead of RuntimeError (#559)”
  • Before: Dir comes from iij/mruby-dir. Reading, iterating or closing a Dir that is closed raises RuntimeError (closed directory), and so does Dir.open with a block when the block closes the Dir. Dir#each and Dir.foreach without a block raise NoMethodError.
  • Now: Dir comes from mruby-dir, a core gem of mruby 4.0.0 that started from iij/mruby-dir and has every method of it. Reading, iterating or closing a Dir that is closed raises IOError (closed directory). Dir.open with a block returns the value of the block also when the block closes the Dir. Dir#each and Dir.foreach without a block return an Enumerator. Dir.foreach with a block closes the Dir that it opened (see “Fixes”), and Dir has methods that iij/mruby-dir does not have (see “New features”).
  • Why it changed:
    • The reason: the v3 plan replaces the third-party gems iij/mruby-env, iij/mruby-process and iij/mruby-dir with core gems of mruby (docs/proposals/v3-plan.md, section 4, Pillar A). Of the three, mruby 4.0.0 has a core gem only for Dir.
    • Why the old behavior could not be kept: the RuntimeError comes from iij/mruby-dir, which raises it for a closed directory unless it is compiled with ENABLE_IO, and the default build does not define it. Keeping the RuntimeError would have meant keeping iij/mruby-dir, which has had no commit since 2018-09-14 and leaves the Dir that Dir.foreach opens open until the GC frees it.
    • What the documentation promised: docs/class_and_method/README.md of 2.x lists the Dir class among the classes of the default build and links it to iij/mruby-dir, whose README lists the methods and says nothing about the exception for a closed Dir.
  • Affected: scripts that rescue RuntimeError around the methods of a Dir that can be closed, or around Dir.open with a block that closes the Dir. When the rescue RuntimeError no longer catches the exception, the handler answers 500 (mrb_run failed: ... error: #<IOError: closed directory>).
  • What to do: rescue IOError. A script that runs on both versions rescues IOError and RuntimeError, or StandardError.

C extensions: an object of a class not declared MRB_TT_DATA cannot wrap C data (#561)

Section titled “C extensions: an object of a class not declared MRB_TT_DATA cannot wrap C data (#561)”
  • Before: mruby 3.x did not check a class whose instance type was not set: a gem that wraps a C struct with Data_Wrap_Struct or mrb_data_object_alloc in an object of its own class got the object.
  • Now: mruby 4.0 raises TypeError: allocation failure of <class> at the allocation, unless the class is declared with MRB_SET_INSTANCE_TT(klass, MRB_TT_DATA).
  • Affected: mrbgems and C extensions that wrap C data in a class without that declaration. matsumotory/mruby-uname at its commits before matsumotory/mruby-uname#1 is one: every method of Uname raises TypeError: allocation failure of Uname, and a handler that calls one answers 500 (the test suite of ngx_mruby saw it, test/t/cases/gem_uname.rb).
  • What to do: declare the class with MRB_SET_INSTANCE_TT(klass, MRB_TT_DATA). The default build pins matsumotory/mruby-uname to a commit that does this (see “Build and test changes”); a build_config.rb of your own that takes mruby-uname needs that commit or a later one.

C extensions: mruby checks the argument count of a C method against its spec (#561)

Section titled “C extensions: mruby checks the argument count of a C method against its spec (#561)”
  • Before: mruby 3.3 checked the argument count of a C method against the argument spec it was defined with (MRB_ARGS_REQ(2) and so on) only when the spec was MRB_ARGS_NONE(). For other specs, only mrb_get_args checked the arguments, against what it reads.
  • Now: mruby 4.0 checks it for every C method before the call: a call with fewer arguments than the spec requires, or, unless the spec has a rest (MRB_ARGS_REST(), MRB_ARGS_ANY()), more than it allows, raises ArgumentError, whatever mrb_get_args would accept. A non-empty keyword hash counts as one argument when the spec takes no keywords. A method defined with MRB_ARGS_REQ(1) that reads "o|o" raises ArgumentError: wrong number of arguments (given 2, expected 1) when called with two arguments, and one defined with MRB_ARGS_REQ(2) that reads "S" raises (given 1, expected 2) when called with one; with mruby 3.3 both calls worked.
  • Affected: C methods of mrbgems and C extensions whose spec differs from what their mrb_get_args reads, in either direction. The C methods of ngx_mruby itself are not affected: for each of the 110 definitions in src/, the spec accepts every argument count that its mrb_get_args accepts.
  • What to do: define the method with the spec that its mrb_get_args reads (for "o|o", MRB_ARGS_ARG(1, 1)).

C extensions: renamed headers and functions, and mrb_open() on failure (#561)

Section titled “C extensions: renamed headers and functions, and mrb_open() on failure (#561)”
  • Before: mruby/ext/io.h, mrb_alloca(), mrb_open_allocf(f, ud) and mrb_open_core(f, ud), which take an allocator of the type mrb_allocf (by default mrb_default_allocf(), which a program could redefine), and mrb_open() returns NULL when a gem initializer fails.
  • Now: mruby/io.h (there is no mruby/ext/io.h), mrb_temp_alloc() (mruby.h keeps mrb_alloca() as a macro for it, “for compatibility”), and no mrb_open_allocf(), mrb_allocf or mrb_default_allocf(): mrb_open_core() takes no arguments, and mruby allocates its memory with mrb_basic_alloc_func(void *ptr, size_t size), which a program can define to replace mruby’s allocator (doc/guides/memory.md of mruby). mrb_open() returns the state with mrb->exc set, instead of NULL, when a gem initializer fails.
  • Affected: C sources of mrbgems and of programs that embed mruby that include mruby/ext/io.h, call mrb_open_allocf() or call mrb_open_core() with arguments (their build stops), that define their own mrb_default_allocf() (mruby no longer calls it), or that test the result of mrb_open() only for NULL. Calls of mrb_alloca() still compile.
  • What to do: include mruby/io.h, call mrb_temp_alloc(), call mrb_open() instead of mrb_open_allocf(f, ud) and mrb_open_core() without arguments, define mrb_basic_alloc_func() instead of mrb_default_allocf() where a program replaces the allocator, and test the result of mrb_open() with MRB_OPEN_FAILURE() (mruby.h), which is true for NULL and for a state with exc set.

C extensions: sources that include mruby.h need the generated headers (#561)

Section titled “C extensions: sources that include mruby.h need the generated headers (#561)”
  • Before: mruby.h did not include mruby/presym.h, so a source that did not use MRB_SYM() compiled with mruby/include alone; presym could also be turned off (MRB_NO_PRESYM). ngx_mruby 2.x compiled its own sources this way: its config.in puts only mruby/src and mruby/include on the include path.
  • Now: mruby.h includes mruby/presym.h, which includes the generated mruby/presym/id.h (mruby/mruby#6733), and MRB_NO_PRESYM is gone, so every source that includes mruby.h needs build/host/include. Compiled with mruby/include alone, such a source stops with fatal error: mruby/presym/id.h: No such file or directory.
  • Affected: C sources compiled outside the mruby build that include mruby.h, among them the sources of ngx_mruby in a build whose ./configure --with-mruby-incdir gives a list of directories without that directory.
  • What to do: add the include directory of the mruby build (mruby/build/host/include) to the include path. The build of ngx_mruby does this for its own sources: the default list of --with-mruby-incdir is <mruby_root>/src <mruby_root>/include <mruby_root>/build/host/include (#559). A list that you give with --with-mruby-incdir needs the directory too, because the sources of ngx_mruby include mruby.h and mruby/presym.h (#572, 3-B. Using Makefile).

The default build leaves out the auto-ssl mrbgem and the classes that came with it (#556)

Section titled “The default build leaves out the auto-ssl mrbgem and the classes that came with it (#556)”
  • Before: the default build_config.rb of 2.x builds the bundled auto-ssl mrbgem (mrbgems/auto-ssl, the Nginx::SSL::ACME classes) and the gems that it depends on: pyama86/mruby-acme-client, pyama86/mruby-polarssl, matsumotory/mruby-httprequest, matsumotory/mruby-simplehttp, mattn/mruby-http, mattn/mruby-base64, takahashim/mruby-forwardable and iij/mruby-tempfile. Scripts can use what these gems define: Nginx::SSL::ACME::Client and Nginx::SSL::ACME::Dehydrated; the classes and modules Acme, CustomHttpRequest, OpenSSL, PolarSSL, HttpRequest, SimpleHttp, HTTP, Base64, Forwardable, FORWARDABLE, Tempfile and TempfilePath; and Dir.tmpdir and Dir.mktmpdir.
  • Now: the default build has none of them. The constants are not defined, Dir.tmpdir and Dir.mktmpdir raise NoMethodError, and a handler that uses one of them answers 500 (mrb_run failed: ... error: #<NameError: uninitialized constant Base64>). The Dir class itself stays. This release has no auto-ssl mrbgem at all (see the next entry).
  • Why it changed:
    • The reason: decision 6 of the v3 plan (docs/proposals/v3-plan.md, section 7) removes auto-ssl from the default build of v3, to be replaced by nginx-acme and by examples that look certificates up in a store. Its ACME client, Nginx::SSL::ACME::Client, speaks ACMEv1, which Let’s Encrypt no longer serves (sections 2.2 and 3.4 of the plan).
    • Why the old behavior could not be kept: the gem cannot get a certificate from Let’s Encrypt, and it brought into every default build eight third-party gems that no other gem of the default build needs, pyama86/mruby-polarssl under the GPL among them. With mruby 4.0.0 the default build would also stop, because pyama86/mruby-polarssl includes mruby/ext/io.h (see the next entry).
    • What the documentation promised: docs/class_and_method/README.md of 2.x calls auto-ssl an optional bundled mrbgem in an early phase of development, without saying whether the default build has it. Its example of mruby_set_code under Nginx::Var Class calls Base64.encode, which the default build had only through auto-ssl.
  • Affected: scripts that use one of the constants above, Dir.tmpdir or Dir.mktmpdir inside nginx.
  • What to do: use [s].pack('m0') in place of Base64.encode(s) and s.unpack1('m') in place of Base64.decode(s), both from the core gem mruby-pack, ENV['TMPDIR'] || '/tmp' in place of Dir.tmpdir, and Dir.mkdir(path, 0700) under a name of your own in place of Dir.mktmpdir, as The auto-ssl mrbgem and the classes that came with it shows. For the other constants, add the gem that defines them with a conf.gem line in a build_config.rb of your own; the gems that depend on pyama86/mruby-polarssl do not build with mruby 4. A server that needs Nginx::SSL::ACME keeps a 2.x build (see the next entry).

The auto-ssl mrbgem and NGX_MRUBY_AUTO_SSL are removed (#640)

Section titled “The auto-ssl mrbgem and NGX_MRUBY_AUTO_SSL are removed (#640)”
  • Before: ngx_mruby 2.x bundles the auto-ssl mrbgem (mrbgems/auto-ssl) and takes it in the default build. It defines Nginx::SSL::ACME::Client, a client of the ACME protocol, and Nginx::SSL::ACME::Dehydrated, helpers for the dehydrated ACME client; test/conf/auto-ssl/ has example configurations for both.
  • Now: this release has no mrbgems/auto-ssl and no test/conf/auto-ssl/, and build_config.rb no longer reads NGX_MRUBY_AUTO_SSL, so no build of this release has Nginx::SSL::ACME. A build with NGX_MRUBY_AUTO_SSL=1 in its environment builds the default gems as without it. A script that uses Nginx::SSL::ACME raises NameError, and its handler answers 500 with a mrb_run failed line in error.log.
  • Why it changed:
    • The reason: decision 6 of the v3 plan (docs/proposals/v3-plan.md, section 7) replaces auto-ssl with nginx-acme and with examples that look certificates up in a store, and the gem cannot work with the bundled mruby. It needs pyama86/mruby-polarssl for TLS, a gem that ngx_mruby does not maintain and that has had no commit since 2017-03-03. That gem includes mruby/ext/io.h, which mruby 4.0 renamed to mruby/io.h, so the mruby build with auto-ssl stops with fatal error: mruby/ext/io.h: No such file or directory (#561). Its ACME client, pyama86/mruby-acme-client, speaks ACMEv1, which Let’s Encrypt turned off in 2021-06 (sections 2.2 and 3.4 of the plan).
    • Why the old behavior could not be kept: the gem cannot be built with mruby 4 until pyama86/mruby-polarssl changes, and even built it could not get a certificate from Let’s Encrypt. Keeping it on next would have kept a gem that no build of v3 can use, with an opt-in variable that ends in a stopped build. ngx_mruby 2.x keeps the gem.
    • What the documentation promised: docs/class_and_method/README.md of 2.x calls auto-ssl an optional bundled mrbgem in an early phase of development and points to the scripts in mrbgems/auto-ssl/mrblib for its API. It says nothing about how long the gem is kept.
  • Affected: scripts that use Nginx::SSL::ACME::Client or Nginx::SSL::ACME::Dehydrated, configurations built from the examples in test/conf/auto-ssl/, and builds that set NGX_MRUBY_AUTO_SSL=1.
  • What to do: get certificates from an ACME server with nginx’s own module ngx_http_acme_module (nginx-acme), or keep a 2.x build for a server that needs Nginx::SSL::ACME; the documentation of 2.x describes the gem. Remove NGX_MRUBY_AUTO_SSL from the environment of your builds: it has no effect.
  • Scripts can use the additions of mruby 3.4 and 4.0: pattern matching (case/in), Array#rfind, trailing commas in method parameters, and &nil in method parameters (#561)
  • Scripts can call methods that mruby 3.3.0 does not have, for example Kernel#public_send, String#b, Hash#slice!, Array#fetch_values, Integer#bit_length, Integer.sqrt and Module#private_instance_methods. mruby’s lists of user-visible changes (doc/mruby3.4.md, doc/mruby4.0.md) name more (#561)
  • Dir.children and Dir#each_child list the entries of a directory without . and .., as in CRuby, and Dir.empty? tells whether a directory has no entries (#559, #561). ngx_mruby 2.x took Dir from iij/mruby-dir, which has none of the three methods. Dir now comes from mruby’s core gem mruby-dir (see Class and Method), and with mruby 4.0.0 the two methods leave out . and .. (since mruby 3.4, mruby/mruby#6265). With mruby 3.3.0, which no release of ngx_mruby combined with mruby-dir, they were aliases of Dir.entries and Dir#each and included . and ..; the test suite of ngx_mruby saw the change (test/t/cases/gem_dir.rb). A script written for such a build that skipped . and .. after these calls can drop that code, or keep using Dir.entries. Dir also includes Enumerable, which iij/mruby-dir does not, so Dir.open(path) { |d| d.select { |name| name.end_with?(".txt") } } works; Dir.open with a block closes the Dir when the block ends.
  • Dir.foreach with a block closes the Dir that it opened. With iij/mruby-dir of 2.x, that Dir kept a file descriptor of the worker open until the GC freed it (#559)
  • When mrb_open() fails while nginx reads the configuration, the http module logs ngx_mruby: mrb_open() failed and the stream module ngx_mruby-stream-module: mrb_open() failed at the emerg level, followed by : <class> raised while initializing mruby when the initialization of mruby or of a gem raised an exception. nginx stops as before; ngx_mruby 2.x logged no line of its own (#559)
  • The installation guide integrates the corrected 2.x build/install lifecycle from #660 while selecting the next development branch, retaining v3 core-gem and lock-file contracts. English/Japanese examples use the same commands; a fresh development build and loopback HTTP trial are recorded with their revision and limits.

  • The site exports version-specific Markdown pages and a JSON documentation index from the same source as HTML, with language, source revision/hash and dirty-preview metadata. Full-body, source and fallback-link checks cover both release lines; display word breaks and SVG are omitted while literal code, tested sample bytes and diagram provenance remain. This does not make a development API stable or certify historical examples as current measurements.

  • The English and Japanese site home pages present the tested nginx configuration, Ruby handler and HTTP exchange together, with a homepage palette and responsive layout. The site checks cover its colors, text scale and shared code highlighting; the rebrand preview extends the navy/graphite surfaces and copper accents to documentation pages.

  • build_config.rb.lock records mruby 4.0.0 (version: 4.0.0, release_no: 40000, written by rake) and no longer has an entry for luisbebop/mruby-polarssl, which the test build no longer clones (#561)

  • The lock pins matsumotory/mruby-uname, in the host and the test build, to 50031c81b101f44e89bf482f5c31213b6c07af36 on the gem’s master (branch: master): the merge commit of matsumotory/mruby-uname#1, which declares the instances of Uname as data objects (MRB_SET_INSTANCE_TT(uname, MRB_TT_DATA)). Its tree is that of the fix commit c82f8a6, and a build no longer depends on the gem’s branch mruby-4 (#561)

  • The test client (the test build of build_config.rb) is built with NO_SSL (ENV['NO_SSL'] ||= '1'), because matsumotory/mruby-simplehttp depends on mruby-polarssl unless NO_SSL is set, and luisbebop/mruby-polarssl depends on mruby-print. SimpleHttp and HttpRequest of the test client cannot send https requests; the tests send HTTPS with curl -k and openssl s_client (docs/test/README.md). rake loads the gems of all builds in one process, so NO_SSL also reaches a host build of your own that has mruby-simplehttp (#561)

  • The default build makes 21 clones (14 in mruby/build/repos/host, 7 in mruby/build/repos/test, about 9.8 MiB), and the build script of matsumotory/mruby-redis clones the hiredis tag v1.5.0 into mruby/build/host/mrbgems/mruby-redis/hiredis-v1.5.0; luisbebop/mruby-polarssl with its submodule ARMmbed/mbedtls and mruby/mgem-list are no longer cloned (#561, #606)

  • The default build includes matsumotory/mruby-redis again, as the default build of 2.7.2 does (“Redis is in the default build again” in the 2.7.2 notes, #604). The lock pins it, in the host build, to 8d939104923bc86bb16d2c203d91e22bfa3196df on the gem’s master (branch: master): the merge commit of matsumotory/mruby-redis#109, which keeps the connection of Redis.connect_set_udptr in the mrb_state instead of mrb->ud (“Redis.connect_set_udptr and Redis.new without arguments keep the connection in the mrb_state” in the 2.7.2 notes). From matsumotory/mruby-redis#108, the build of that commit clones the hiredis tag v1.5.0, checks its commit 5bb7f030f6631b243438cf0fdd3be84352b905bb and builds it with USE_WERROR=0, and the gem declares Redis#ttl with one argument: mruby 4.0.0 checks the argument count of a C method against its declaration, and with the gem’s earlier declaration r.ttl "key" raised ArgumentError. The lock also pins matsumotory/mruby-pointer, which mruby-redis depends on and which adds no class, to 8b46506dfdb12a79a16be1d1172ef3c9324bef96 on its master (#606)

  • mruby/ holds the tree of the mruby tag 4.0.0 (commit 831da26b9021), merged as a subtree squash that records git-subtree-split: 831da26b9021de0369d17b71b5667e2941a1a32d, so the next update-mruby-subtree merges from that commit. docs/DEVELOPMENT.md describes how to finish an update-mruby-subtree that stops with conflicts under mruby/: replace the subtree with the exact tree of the ref with git read-tree, check that git write-tree --prefix=mruby/ and git rev-parse REF^{tree} print the same id, and commit the merge alone, with the build changes in the commits that follow (#561)

  • NGX_MRUBY_ALLOW_MISSING_CORE_GEMS=1 in the environment of the mruby build skips, with a notice, the names in NGX_MRUBY_CORE_GEMS that the mruby being built does not have, where the build would otherwise stop. It is meant for the time of an update of the bundled mruby (docs/DEVELOPMENT.md) (#559)

  • The memory soak test: sh test/soak/run.sh builds ngx_mruby in build_soak/ with -DNGX_MRUBY_DEBUG_STATS, which adds the class Nginx::Debug (Nginx::Debug.gc, and Nginx::Debug.stats with the GC registrations that ngx_mruby counts), and runs its scenarios against it. It listens on 12360 and 12361, and starts a mock LLM upstream on 12362 for the agent proxy scenarios. The default builds of build.sh and test.sh do not have Nginx::Debug. The soak job of CI runs the test, and on next the required check ci-ok needs that job (“Soak test for memory” in docs/test/README.md) (#542, #554, #557)

  • The performance comparison: sh test/perf/compare.sh BASE_DIR builds the checkout in BASE_DIR and this checkout in build_perf/, and compares the instructions per request that callgrind counts in each scenario. It listens on 12370 and 12371, and starts the mock LLM upstream on 12372. The perf job of CI runs it for pull requests that change an input of the measured binary, the measurement or the workflow (“Performance comparison with callgrind” in docs/test/README.md) (#549, #554)

  • The agent proxy tests: test/t/cases/agent_proxy.rb sends requests to the agent proxy recipes of test/conf/conf.d/60-agent-proxy.conf on port 18132, with a mock LLM upstream (test/soak/mock_llm.rb) that test/t/cases/_agent_proxy_client.rb starts on 12372 and 12373 (“Mock LLM upstream” in docs/test/README.md) (#554)

  • The mock LLM upstream test/soak/mock_llm.rb also answers POST /v1/responses as the OpenAI Responses API does, with and without a stream. A stream ends with response.completed, response.incomplete or response.failed, chosen with --terminal or the request header x-mock-terminal. The perf lane measures responses_stream_plain_50 in its default set and responses_stream_plain_1000 when PERF_SCENARIOS names it, the soak test runs agent_responses_stream, and test/t/cases/agent_proxy.rb checks its recipes on /v1/responses as well. No port is added (“Mock LLM upstream” in docs/test/README.md) (#600)

  • New test cases and the ports of their servers: test/t/cases/gem_dir.rb (Dir, 18127) and test/t/cases/gem_uname.rb (mruby-uname, 18128) (#559), test/t/cases/config_time_log.rb (the messages logged while nginx reads the configuration, 18140) (#565), test/t/cases/return_in_block.rb (a return inside a block or a for body of handler code, 18117) (#584), test/t/cases/var_method_lookup.rb (the accessors of Nginx::Var and the methods that read variables, 18159) (#610), test/t/cases/home_example.rb (the example of the home page of the site, 18160) (#617), and test/t/cases/guides.rb (the samples of the guides and use cases of the site, 18161 to 18168 and the stream port 12364) (#619)

  • dependence/ngx_devel_kit/ holds the tree of the ngx_devel_kit tag v0.3.4 (commit bd44d1630), with the one change of ngx_mruby kept: in src/ndk_set_var.c, an NGX_DONE that the variable handler of mruby_set returns becomes the status of nginx’s script engine. The tree was merged with update-devkit-subtree v0.3.4 as a subtree squash that records git-subtree-split: bd44d16302273052d6005d7bdb55f74e23813de3, so the next update-devkit-subtree merges from that commit. The tree before was the tag v0.3.1 (a22dade76), whose src/ndk.h said 0.2.15. ngx_mruby enables only the set_var part of NDK (have=NDK_SET_VAR in config.in). Of the files that such a build compiles, the commits since v0.3.1 change only src/ndk.h and src/ndk_string_util.h, in lines that produce no machine code: the version macros of src/ndk.h are used nowhere, NDK’s config always defines NDK, so the removed #error never applied, and ndk_memcpyp is a macro whose only user, src/ndk_conf_file.c, is not compiled. The commits fix the argument that ndk_conf_set_http_complex_path_slot passes to its post handler and the size that ndk_memcpyp copies, correct the type in the size of the bucket array that ndk_upstream_list allocates (the number of bytes stays the same), remove the #error that src/ndk.h raised in a build without NDK, and set NDK_VERSION to “0.3.4”. A build that also adds a module which enables those parts of NDK compiles the fixed code (#597)

  • nginx’s own test suite: sh test/nginx-tests/run.sh runs nginx-tests at a pinned commit with prove on nginx built with ngx_mruby by test.sh and on nginx of the same version and configure options without ngx_mruby, and writes the counts of both builds and the test files that fail on each to build_nginxtests/summary.md. It needs Linux, a user other than root and perl, and the test files listen on ports from 8000 to 8999. TEST_SH_SKIP_TESTS=1 sh test.sh stops after the build and the install. The nginx-tests workflow of CI runs it on pull requests to next that change the nginx binary, test.sh or the runner, and on pushes to next; the required check ci-ok does not need it (“nginx’s own test suite (nginx-tests)” in docs/test/README.md) (#602)

  • The memory soak test also checks Nginx::Debug itself: in each scenario of test/soak/nginx.conf it requests the new location /debug/check once before the warmup, and the scenario fails unless Nginx::Debug.stats has the documented keys with Integer values, Nginx::Debug.gc returns nil, and the full GC of Nginx::Debug.gc frees the objects that nothing references (“Soak test for memory” in docs/test/README.md) (#605)

  • The example of the start page of the site: examples/start/ holds a compose.yaml and a Dockerfile that build nginx with ngx_mruby from the checkout, with an nginx.conf and a Ruby script, and its README.md is the page /start/. The examples workflow of CI runs the commands of that README in order with examples/check_example.py and compares their output with what the README states, on pull requests to next and pushes to next that change examples/, the workflow or the inputs of the build (src/, mrbgems/, mruby/, dependence/, build.sh, build_config.rb, build_config.rb.lock, nginx_version, configure, config.in, Makefile.in); the required check ci-ok does not need it (#618)

  • test/t/cases/agent_proxy.rb reads the values that it compares for the recipes of the Messages API from test/html/agent_proxy_auth.expected, agent_proxy_route.expected, agent_proxy_limit.expected and agent_proxy_log.expected, one key=value per line, or key~=text for a value that contains the text, which the agent proxy page of the site shows; test/t/cases/_agent_proxy_client.rb takes the port of nginx as an optional third argument (#622)

  • test.sh stops only the nginx that it started, by its pid. It no longer runs killall nginx (pkill nginx on NetBSD, FreeBSD and CentOS) before it starts the test nginx, which stopped every nginx on the machine. Before the start, it stops the nginx that an earlier run in the same build directory left running, by the pid in logs/nginx.pid, when that process runs the sbin/nginx of this build. When it exits, also after a failure and on SIGHUP, SIGINT or SIGTERM (once the command that runs at that moment, such as the wait for nginx or the current test file, has ended), it sends SIGTERM to the test nginx and sends it again 1, 3, 7, 15 and 31 seconds later while the process still runs, because a single nginx process can miss a SIGTERM that arrives right before it waits for events; after NGINX_STOPTIME seconds (default 60) it sends SIGKILL and exits 1. Before each signal, it checks that the pid still runs the sbin/nginx of this build, so a pid that another process took after nginx ended gets no signal. test/t/cases/_second_instance.rb sends the SIGQUIT to its nginx again in the same way (“Stopping nginx” in docs/test/README.md) (#635)

  • TEST_SH_PORT_OFFSET=N (an integer from -2345 to 47336, default 0) moves every port of the test suite by N, so that two checkouts can run test.sh at the same time on one machine, each with its own offset. The ports of the suite are the numbers from 12345 to 12399 and from 18080 to 18199 in test/t, test/conf, test/html and the two files of test/soak that the agent proxy tests load. test.sh copies these files with the new script test/t/cases/_stage.rb to build/nginx/harness/test/, moving each such number written as five digits by N, and installs the configuration and the hook scripts and runs the tests from that copy; the tests run the CRuby helpers of the copy through the new method suite_path of test/t/cases/_prelude.rb. With offset 0 the copy is the same as the source. _stage.rb fails for such a number written with an underscore and for an offset that would move a port out of 10000 to 65535 (“Ports” in docs/test/README.md) (#641)

  • The message that test.sh prints when it sends SIGKILL to the test nginx appears only when it sent the signal: when the pid no longer runs this build’s nginx at that moment, the stop ends without the signal and counts as ended, so test.sh keeps the exit status of the tests. Before, it printed that it sent SIGKILL, and test.sh exited 1; at the start, the stop of the nginx left by an earlier run ended the run (#641)

  • valgrind fails the CI cell that runs nginx under it: such a cell runs nginx with valgrind --error-exitcode=99 --leak-check=full --errors-for-leak-kinds=definite,indirect and the suppressions of the new file test/valgrind.supp, and its step “Check the valgrind reports” fails the cell when the ERROR SUMMARY of a valgrind log counts an error: a memory error, or a definitely or indirectly lost block that the file does not suppress. The file suppresses the environment array that nginx leaves allocated at exit (ngx_set_environment(), 8 bytes). Possibly lost blocks are printed but are not errors. On pull requests and pushes, one cell of the build matrix (nginx 1.31.6, static module) runs nginx under valgrind, and the other cells without a sanitizer run it directly. A manual run of test.yml with the new input valgrind runs every cell except the sanitizer cell under valgrind; the new workflow valgrind-nightly.yml starts such a run every day once the file is on master, the default branch, and until then on pull requests that change it, test.yml or test/valgrind.supp. test.sh still does not take valgrind’s errors into its exit status (“valgrind in CI” in docs/test/README.md) (#634)

  • The site is deployed: the job deploy of .github/workflows/site.yml publishes the site that the job build built and checked to GitHub Pages at https://ngx.mruby.org/, on a push to next and on a workflow_dispatch on next, never for a pull request. It uses actions/configure-pages, actions/upload-pages-artifact and actions/deploy-pages, pinned by commit, with the permissions pages: write and id-token: write only, and stops unless the Pages settings give the origin https://ngx.mruby.org. The site serves the Google verification file googleba3435e4002729b6.html of the old site at the same URL and loads no analytics script, which site/scripts/check-deploy.mjs checks on every run; site/scripts/check-live.mjs checks the live site after a deployment, by hand (“The site” in AGENTS.md) (#653)

  • The site builds the documentation of 2.x under /v2/ from docs/ of master: node site/scripts/build-v2.mjs --tree DIR (npm run build:v2 in site/), after npm run build, writes the 2.x pages into site/dist/v2/ with a search index of their own, and site/scripts/check-v2.mjs checks the 2.x banner on every 2.x page, the version links between /v2/ and /docs/, and that the search of each version shows only its own pages. The site workflow checks out master and builds /v2/ on every run. On master, the site-v2 workflow builds the site with the docs/ of every pull request to master and fails on a broken /v2/ link, and the site-dispatch workflow starts the site workflow on next after every push to master (#657, #656)

Supported nginx versions: the nginx mainline and stable lines (see README.md). Read the release notes of the 2.x releases newer than the version you run first, then this file. Rebuild the module together with the bundled mruby 4.0.0; a build_config.rb of your own needs the changes of the second entry above. Migrating from ngx_mruby 2.x to 3.0.0 has the steps for finding the scripts and builds that each entry affects. Check these before upgrading:

  • The bundled mruby 4.0.0: rebuild the module together with it, and check mruby’s lists for the changes that these entries do not cover (#561).
  • A build_config.rb of your own: remove mruby-print, add hal-posix-io, hal-posix-dir and hal-posix-socket, and decide on mruby-task and mruby-encoding (#561).
  • build_config.rb.lock: nothing for a new checkout or a source archive; to build a gem at another commit, change its entry, and remove mruby/build after a change of the lock (#558).
  • Hash#inspect and Hash#to_s: do not use them as a data format; build the string yourself, or use JSON.generate (#561).
  • Exception#inspect: update log parsers and alerts that match the old form of the mrb_run failed lines (#561).
  • The messages logged while nginx reads the configuration: to see them, run nginx -t -e stderr, read the startup log after nginx -t, or reload with error_log at info (#565).
  • NoMethodError: match on the exception class, not on the message (#561).
  • private and protected: call such methods without a receiver (Integer(x) instead of Kernel.Integer(x)), or with send; list private methods with private_instance_methods or private_methods (#561).
  • Module#remove_const, #private, #public, #protected, #module_function and the hooks: call them in the body of the class or module without a receiver, or with send (#561).
  • Overrides of append_features, prepend_features and extend_object: move the code to included, prepended or extended (#561).
  • allbits?, anybits? and nobits? on a Float: convert with to_i first (#561).
  • FileTest: nothing for calls of its methods (#561).
  • A constant assigned in a method body: use Object.const_set (or Kernel.const_set) (#561).
  • Bit operations on a Float: convert with to_i first, or compute the old number as the entry says (#561).
  • String#valid_encoding?: drop the call, or build with mruby-encoding and check the scripts (#561).
  • A seeded Random or srand: do not depend on its numbers across versions (#561).
  • Set#inspect, Set#to_s and Set#flatten_merge: do not use the first two as a data format; call s.merge(Set.new(t).flatten) in place of s.flatten_merge(t) (#561).
  • Hash#default_proc=: pass a Proc or nil (#561).
  • yield with keyword arguments: nothing (#561).
  • A return inside a block or a for body of handler code: nothing (#561).
  • A content handler that ends without output: a handler that relied on the 500 sets the status it means, for example Nginx::HTTP_INTERNAL_SERVER_ERROR; log monitoring that matches response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERROR no longer sees such handlers of the main request with no status or a status from 200 to 299 that nginx does not answer by itself (#574).
  • An output body filter that ends with an empty body: nothing (#574).
  • The accessors of Nginx::Var: use v.exist?(name) to tell whether a variable exists, and give the methods of your scripts names that are not names of nginx variables (#610).
  • Nginx::Connection, Nginx::Server and Nginx::Request: to return other values, define the methods on those classes; read variables with Nginx::Var instead of @iv_var (#610).
  • Dir: rescue IOError instead of RuntimeError around a Dir that can be closed; a script for both versions rescues both (#559).
  • C extensions: declare classes that wrap C data with MRB_SET_INSTANCE_TT(klass, MRB_TT_DATA), define C methods with the spec that mrb_get_args reads, use mruby/io.h, mrb_temp_alloc() and MRB_OPEN_FAILURE(), call mrb_open() or mrb_open_core() without an allocator and define mrb_basic_alloc_func() for a custom one, and add mruby/build/host/include to the include path, also in a list given with --with-mruby-incdir (#561, #572).
  • The classes that came with the auto-ssl mrbgem: use pack('m0') and unpack1('m') in place of Base64, ENV['TMPDIR'] || '/tmp' in place of Dir.tmpdir and Dir.mkdir in place of Dir.mktmpdir, or add the gems to a build_config.rb of your own (#556).
  • The auto-ssl mrbgem: get certificates with ngx_http_acme_module, or keep a 2.x build for a server that needs Nginx::SSL::ACME; remove NGX_MRUBY_AUTO_SSL from the build environment (#640).