ngx_mruby v3.0.0
Behavior changes: read before upgrading
Section titled “Behavior changes: read before upgrading”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.gemboxof mruby 3.3.0 selects, mruby-print included. - Now: this release bundles mruby 4.0.0, the tag
4.0.0of mruby (commit831da26b9021, 2026-04-20). The defaultbuild_config.rbtakes the core gems thatNGX_MRUBY_CORE_GEMSnames: the set thatfull-core.gemboxof mruby 3.3.0 selected, without mruby-print (mruby 3.4 removed it;Kernel#print,#putsand#pcome 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), whichfull-core.gemboxof 4.0.0 takes, and keeps mruby-sleep, whichfull-core.gemboxof 4.0.0 leaves out. The line that nginx logs at theinfolevel 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 indoc/mruby3.4.mdanddoc/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, andnextmoves 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.rbof your own could name mruby-print, or takefull-core.gemboxand 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_gemsinbuild_config.rbstops the build on a name that the bundled mruby does not have.full-core.gemboxof 4.0.0 takes mruby-task, which redefinesKernel#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 ofString#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/sin a container chose the Windows back ends by itself and stopped atwindows.h: No such file or directory, becausefor_windows?inmruby/lib/mruby/gem.rbtreats a one-letter directory at the root as a Windows drive. - Affected: builds with a
build_config.rbother than the one of this release that name mruby-print or takefull-core.gembox. - What to do: start from the
build_config.rbof this release, or select the core gems by name asNGX_MRUBY_CORE_GEMSdoes: 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.rbnames the third-party mrbgems without a commit, and.gitignorelists thebuild_config.rb.lockthat 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 withgit 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.lockis 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 abuild_config.rbof your own adds is cloned at the head of its branch, and rake adds its entry to the lock in your tree, whichgit statusthen shows as modified. rake does not fetch into a clone that exists: after a change of the lock, a build in a tree that keepsmruby/buildstops withfatal: reference is not a treewhen 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.
- The reason: the v3 plan pins every third-party gem by commit and commits
- Affected: builds from source that expect the newest commits of the third-party gems,
build_config.rbfiles of your own that add gems, and trees that keepmruby/buildwhen 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/buildbefore 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)andArray#inspect. - Affected: scripts that put the
inspectorto_sof 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_requestintest/t/ngx_mruby.rb). - What to do: do not use the
inspectorto_sof a Hash as a data format, in a response body or in a header. Build the string yourself, or useJSON.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 withException#inspectinto themrb_run failedlines oferror.log(levelerror; the line that an exception inmruby_stream_server_context_codelogs while nginx reads the configuration,mrb_run failed: ngx_mruby stream module configuration failed: error: ..., is at levelemerg), for examplemrb_run failed: return 500 HTTP status code to client: error: boom (RuntimeError). - Now:
#<RuntimeError: boom>, so the line ends inerror: #<RuntimeError: boom>.Exception#messageand#to_sare 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 inerror.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
noticelevel:ngx_mruby/<version> (mruby/<version>) mechanism enabled, once for thehttp {}block;ngx_mruby NOTICE ...: compile info: ...for each directive with Ruby code, andngx_mruby-stream-module NOTICE ...: compile info: ...for each one instream {}; andngx_mruby NOTICE ...: target variable=(...)for eachmruby_setandmruby_set_code. nginx writes what it logs while it reads the configuration to its startup log, at thenoticelevel whatevererror_logsays, so every start of nginx writes these lines, also witherror_log ... warn;. - Now: the same messages are logged at the
infolevel, and their text saysINFOinstead ofNOTICE(ngx_mruby INFO ...,ngx_mruby-stream-module INFO ...). A start of nginx writes none of them.nginx -twrites them at[info]to the startup log only: nginx copies a line of the startup log to the terminal only atwarnand above, so the output ofnginx -tdoes not show them. A reload writes them to theerror_logof the running configuration when thaterror_logis atinfoordebug. Errors while nginx reads the configuration, such as a script file that cannot be opened, are still logged atemerg. - Why it changed:
- The defect: every start of nginx wrote these lines, and an
error_logat a level abovenoticedid 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
noticelevel, before the configurederror_logtakes over, so noerror_logsetting filters a message atnotice. Only a level belownoticekeeps the lines out of a start. - What the documentation promised: the documentation of 2.x said nothing about these messages.
- The defect: every start of nginx wrote these lines, and an
- 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 afternginx -t; or reload witherror_logatinfo. The startup log is the file given with-e, or else the error log path compiled into nginx (logs/error.logunder 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.
private and protected are enforced (#561)
Section titled “private and protected are enforced (#561)”- Before: not enforced: a method defined after
privateorprotected,initialize, a method defined withdefat the top level, and theKernelmethods could be called with an explicit receiver,Kernelitself included (Kernel.Integer("12")is12).Module#instance_methodsand#methodsof an object list private methods too: for a class with a methodpuband a private methodpriv,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 thanself. This includes theKernelmethods such asraise,sprintf,puts,Integerandrand, andinitialize_copy,respond_to_missing?andmethod_missing, which mruby 4.0.0 defines as private ("a".initialize_copy("b")raises). Called onKernelitself, onlyKernel.format,Kernel.sprintf,Kernel.raise,Kernel.sleepandKernel.usleepstill work; the other private methods ofKernel, among themInteger,Float,String,Array,Hash,eval,randandputs, raiseNoMethodError(Kernel.Integer("12")raisesprivate method 'Integer' called for Module).respond_to?returnstruefor a private method on both versions (Kernel.respond_to?(:Integer)istrue), so it does not tell whether such a call works.Module#instance_methodsand#methodsof an object leave private methods out (instance_methods(false)of the class above is[:pub]);Module#private_instance_methodsand#private_methodslist them. In handler code, a method defined withdefat 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 onKernelincluded, and scripts that look for private methods in the result ofinstance_methodsormethods. - What to do: call them without a receiver (
Integer(x)instead ofKernel.Integer(x)), or withsend(Kernel.send(:Integer, x)works on both versions). To list private methods, useprivate_instance_methodsorprivate_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)andobj.singleton_method_added(:m)work. The hooks are#included,#extended,#prepended,#inheritedand#method_addedofModuleandClass, andBasicObject#singleton_method_added. - Now: private: called with an explicit receiver, they raise
NoMethodError: private method ... called.sendworks, and mruby still calls the hooks itself:prependcallsprepended, anddef obj.mcallssingleton_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,prependandextendcall these three methods, so a module can override them. - Now: the three methods are gone (
Module.new.respond_to?(:append_features, true)isfalse, and the same for the other two).include,prependandextendadd the module without calling them, so an override is skipped without an error. The hooksincluded,prependedandextendedare still called. - Affected: modules that override one of the three methods.
- What to do: move the code to
included,prependedorextended.
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)istrue). - Now: methods of
Integeronly; on a Float they raiseNoMethodError. On an Integer they work as before (6.allbits?(2)istrue). - Affected: scripts that call them on a Float.
- What to do: convert a Float with
to_ifirst.
FileTest is a module (#561)
Section titled “FileTest is a module (#561)”- Before:
FileTestis a class. - Now:
FileTestis a module.FileTest.exist?and its other methods are called as before. - Affected: scripts that use
FileTestas 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#runof ngx_mruby setsServerwithKernel.const_set. - What to do: use
Object.const_set(:X, 1)(orKernel.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 & 1is1,~1.5is-2). For a shift count from 1 up,<<multiplies the Float by that power of two and truncates (1.5 << 1is3,-1.5 << 1is-3,1.5 << 40is1649267441664), and gives a Float when the result is outside the Integer range (1.5 << 70is1.77088743107612e+21). For a shift count from 1 to 32,>>divides a positive Float by that power of two and truncates (8.0 >> 1is4,1.5 >> 1is0), and returns-1for every negative Float (-8.0 >> 1is-1). For a count above 32,>>returns0for a positive and-1for a negative Float (1e20 >> 32is23283064365,1e20 >> 33is0). A count of 0 returns the Float unchanged (-1.5 >> 0is-1.5), and a negative count shifts the other way (1.5 >> -1is3). - Now:
~,&,|,^,>>and<<raiseNoMethodErroron 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 withto_ifirst. For<<and>>,to_ifirst can give another number (1.5.to_i << 1is2,-8.0.to_i >> 1is-4). Where the old number matters: for<<by a count from 1 up, multiply the Float by the power of two and callto_i; for>>by a count from 1 to 32, divide a positive Float by the power of two and callto_i, and use-1for a negative Float; for>>by a count above 32, use0for a positive and-1for a negative Float (dividing would give another number:(1e20 / 2**40).to_iis90949470, where1e20 >> 40was0); 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 withoutMRB_UTF8_STRING, and the method returnstruefor every string, even"\xfe". A build whosebuild_config.rbdefinesMRB_UTF8_STRINGgetsfalsefor 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.rbof 2.x, drop the call (it never returnedfalsethere), or build with mruby-encoding and check the scripts as Core gems says. A 2.x build that definedMRB_UTF8_STRINGneeds mruby-encoding to keep the check. Why the default build leaves mruby-encoding out: the gem definesMRB_UTF8_STRINGfor the whole build, soString#size,#[]and#indexcount characters instead of bytes, and a script that computes aContent-LengthfromString#sizewould 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)andsrand(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_mergeis 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)andArray#inspect. mruby 4.0.0 removedSet#flatten_merge, and a call raisesNoMethodError; mruby’s lists of user-visible changes do not name it. - Affected: scripts that use the
inspectorto_sof a Set as a data format, or compare it with a fixed string, and scripts that callSet#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 ofs.flatten_merge(t), calls.merge(Set.new(t).flatten), which gives on both versions the Set thats.flatten_merge(t)gave on 2.x, for a Set and for an Arrayt.
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
returninside a block, such as[0].each do ... endor3.times { ... }, or inside the body offor, at the top level of handler code raisedLocalJumpError, and the handler answered 500 withmrb_run failed: return 500 HTTP status code to client: error: unexpected return (LocalJumpError)inerror.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 afterNginx.rputs(entry 3 of the 2.7.1 release notes). The variable ofmruby_set_codegot an empty value. Areturnat the top level of the code, outside a block, ended the code. - Now: such a
returnends the code, as areturnat 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 thatNginx.returnset and the output thatNginx.rputswrote. Inmruby_set_code, the value of areturnin a block becomes the variable.error.loghas no line for it. - Why it changed:
- The defect: #502: the
returnthat ends a handler early raisedLocalJumpErrorwhen it was written inside a block or aforbody, 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
returnleaves, 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_ of 2.x says that ngx_mruby supports themethod/README.md returnstatement in handler code and that it returns from the handler, with no exception for a block.
- The defect: #502: the
- Affected: handler code with a
returninside a block or the body offorat its top level; areturninside a method or a lambda is not affected. Log alerts and scripts that relied on the 500 or on theLocalJumpErrorline are affected as well. - What to do: nothing. The two ways around it on 2.x, a
returnat 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.
selfismain, local variables belong to one run of the code, and constants, globals, instance variables ofmainand methods defined withdefpersist 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
""ornilwithNginx.rputs, or only set response headers) got 500 with nginx’s error page (or theerror_pageconfigured for 500) andConnection: closewhen it set no status,Nginx::HTTP_OK(withNginx.return,Nginx.send_headerorNginx.status_code=) or another status below 300 other than 201 and 204 (for exampleNginx::HTTP_ACCEPTED), also afterNginx::Async::HTTP.sub_requestresumed it (entry 13 of the 2.7.1 release notes). error.log got a line at theerrorlevel: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_ERRORorstatus code is <N>, but response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERROR. On 2.7.0 and earlier,Nginx::HTTP_OKwithout 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 untilkeepalive_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: 0and an empty body. It sends noContent-Type, as ngx_mruby’s answers with output do not; nginx’sreturn 200;sends the one ofdefault_type. A HEAD request gets the same header. The header filters and body filters of the location (mruby_output_header_filter,mruby_output_body_filterand their_codeforms) 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_OKand no body answered 500, where nginx’s ownreturn 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), whichContent-Length: 0does. 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 thatNginx.return“Returns nginx status code to send back HTTP status to the client” (Nginx.return of 2.7.0).
- The defect: #200: a content handler that ended with
- Affected:
mruby_content_handlerandmruby_content_handler_codehandlers 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 orniltoNginx.rputs, one that only sets response headers, and one that ends withNginx.return Nginx::HTTP_OKand no body, and log monitoring of the lines above. Unchanged: a status below 200 without output (500 and thestatus code is <N>, but response body is emptyline, 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_OKwithout output in a rewrite or access handler still answers 500 and logsstatus code is 200, but response body is empty), and subrequests (Nginx::Async::HTTP.sub_request, SSI includes andauth_request): the content handler of a subrequest that returnsNginx::HTTP_OKwithout 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 exampleNginx.return Nginx::HTTP_INTERNAL_SERVER_ERRORorNginx.return Nginx::HTTP_NOT_FOUND, which nginx answers as before. Log monitoring that matchesresponse body is empty. return NGX_HTTP_INTERNAL_SERVER_ERRORfinds only the statuses below 200 of content handlers, andNginx::HTTP_OKin 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 twoPOSTrequests whose body the handler does not read on one connection), and the cases “Nginx.return 200 dont have body” oftest/t/ngx_mruby.rband “send_header 200 without a body answers 200 with an empty body” oftest/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""withNginx::Filter#body=orNginx::Filter#output, or because the response had no body and the filter left it empty, nginx loggedzero size buf in writerat thealertlevel 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: 0and an empty body, and error.log gets noalertline 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 ownreturn 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.rbof 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 says that the response is sent with exactly the body that the filter sets, and that#body= of 2.7.1 Content-Lengthis its length, with no exception for an empty String.
- 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 (
- 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 writerline, 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.NAMEandv.NAME = valueon anNginx::Varwent throughNginx::Var#method_missing.Nginx::Varhad three methods of its own,method_missing,exist?andset:v.respond_to?(:request_method)returnedfalse, andNginx::Var.instance_methods(false)listed only those three. When a script defined a method onObjectorKernelwhose name was also the name of a variable, for example with adefat the top level of handler code,v.NAMEcalled that method instead of reading the variable. - Now: the first
v.NAMEorv.NAME = valueof a name in a worker process also definesNAMEorNAME=as a method ofNginx::Var, and the later accesses of the name in that worker call the method. The method reads and writes the variable asmethod_missingdoes: the values,nilfor a name that is not a variable, the exceptions and the lines oferror.logare 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 thatsetdeclares), and a getter for the first 16 names of prefix variables that scripts use, such asarg_qorhttp_x_request_id. A name gets a method only when it is made ofatoz,0to9and_, with=at the end for a setter;v.Request_Methodkeeps reading$request_methodthroughmethod_missing. A name that is not a variable, a setter of a prefix variable, an instance of a subclass ofNginx::Var, a frozenNginx::Var, and a name thatNginx::Varor 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)andNginx::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 onObjectorKernelno longer hides the variable fromv.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,KernelandBasicObject, more than 110 entries, and mruby does not cache a name that it did not find. Eachv.NAMEpaid that search beforemethod_missingran. In the callgrind comparison of #561, the search made 74% of the difference of thevarscenario and 59% of that of theauthscenario 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.NAMEskip 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 thatNginx::Var#method_missingis called whenNginx::Var#VARNAMEorNginx::Var#VARNAME=is evaluated. It said nothing aboutrespond_to?or the methods ofNginx::Var.
- The reason: with mruby 4.0.0, the lookup of a method name that no class defines compares the name with every method of
- Affected: scripts that call
respond_to?,methods,instance_methodsormethod_defined?onNginx::Varor its instances, for example to tell whether a name is a variable, scripts that define a method onObjectorKernelwith the name of a variable that they also read withv.NAME, and scripts that wrapNginx::Var#method_missing, for example with a module prepended toNginx::Varwhosemethod_missingcallssuper: 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_ipand#local_port,Nginx::Server#document_root,#realpath_rootand#path, andNginx::Request#hostname,#authority,#filename,#userand#get_bodycalled the accessor of their variable (remote_addr,remote_port,server_addr,server_port,document_root,realpath_root,host,http_host,request_filename,remote_userandrequest_body) on anNginx::Varthat the object created and kept in its instance variable@iv_var. A script that defined such a method onNginx::Var, defined amethod_missingof 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 anNginx::Varnor set@iv_var.Nginx::Request#varstill creates theNginx::Varof the object and keeps it in@iv_var. Outside a request they raise the sameRuntimeErroras before. - Why it changed:
- The reason: the names of these variables are fixed, and “Design principles” in
AGENTS.mdasks 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 onNginx::Varon each call, which takes the search of the entry above, and each object created anNginx::Varand 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_varis 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 thatremote_ip,remote_port,local_ipandlocal_portare the same as the nginx variables$remote_addr,$remote_port,$server_addrand$server_port, and Nginx::Server says the same of#realpath_ root $realpath_root. That is what they return now in every case.
- The reason: the names of these variables are fixed, and “Design principles” in
- Affected: scripts that change what these methods return by defining methods on
Nginx::Var, amethod_missingof their own on it, or another object in@iv_var, and scripts that read@iv_varof these objects without callingNginx::Request#varfirst. - What to do: to return another value, define the method on
Nginx::Connection,Nginx::ServerorNginx::Requestitself, or call a method of your own. Read variables withNginx::Var(Nginx::Request#var,Nginx.varorNginx::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:
Dircomes from iij/mruby-dir. Reading, iterating or closing aDirthat is closed raisesRuntimeError(closed directory), and so doesDir.openwith a block when the block closes theDir.Dir#eachandDir.foreachwithout a block raiseNoMethodError. - Now:
Dircomes 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 aDirthat is closed raisesIOError(closed directory).Dir.openwith a block returns the value of the block also when the block closes theDir.Dir#eachandDir.foreachwithout a block return anEnumerator.Dir.foreachwith a block closes theDirthat it opened (see “Fixes”), andDirhas 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 forDir. - Why the old behavior could not be kept: the
RuntimeErrorcomes from iij/mruby-dir, which raises it for a closed directory unless it is compiled withENABLE_IO, and the default build does not define it. Keeping theRuntimeErrorwould have meant keeping iij/mruby-dir, which has had no commit since 2018-09-14 and leaves theDirthatDir.foreachopens open until the GC frees it. - What the documentation promised: docs/class_
and_ of 2.x lists themethod/README.md Dirclass 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 closedDir.
- 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 (
- Affected: scripts that rescue
RuntimeErroraround the methods of aDirthat can be closed, or aroundDir.openwith a block that closes theDir. When therescue RuntimeErrorno 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 rescuesIOErrorandRuntimeError, orStandardError.
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_Structormrb_data_object_allocin 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 withMRB_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
UnameraisesTypeError: 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”); abuild_config.rbof 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 wasMRB_ARGS_NONE(). For other specs, onlymrb_get_argschecked 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, raisesArgumentError, whatevermrb_get_argswould accept. A non-empty keyword hash counts as one argument when the spec takes no keywords. A method defined withMRB_ARGS_REQ(1)that reads"o|o"raisesArgumentError: wrong number of arguments (given 2, expected 1)when called with two arguments, and one defined withMRB_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_argsreads, in either direction. The C methods of ngx_mruby itself are not affected: for each of the 110 definitions insrc/, the spec accepts every argument count that itsmrb_get_argsaccepts. - What to do: define the method with the spec that its
mrb_get_argsreads (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)andmrb_open_core(f, ud), which take an allocator of the typemrb_allocf(by defaultmrb_default_allocf(), which a program could redefine), andmrb_open()returnsNULLwhen a gem initializer fails. - Now:
mruby/io.h(there is nomruby/ext/io.h),mrb_temp_alloc()(mruby.hkeepsmrb_alloca()as a macro for it, “for compatibility”), and nomrb_open_allocf(),mrb_allocformrb_default_allocf():mrb_open_core()takes no arguments, and mruby allocates its memory withmrb_basic_alloc_func(void *ptr, size_t size), which a program can define to replace mruby’s allocator (doc/guides/memory.mdof mruby).mrb_open()returns the state withmrb->excset, instead ofNULL, when a gem initializer fails. - Affected: C sources of mrbgems and of programs that embed mruby that include
mruby/ext/io.h, callmrb_open_allocf()or callmrb_open_core()with arguments (their build stops), that define their ownmrb_default_allocf()(mruby no longer calls it), or that test the result ofmrb_open()only forNULL. Calls ofmrb_alloca()still compile. - What to do: include
mruby/io.h, callmrb_temp_alloc(), callmrb_open()instead ofmrb_open_allocf(f, ud)andmrb_open_core()without arguments, definemrb_basic_alloc_func()instead ofmrb_default_allocf()where a program replaces the allocator, and test the result ofmrb_open()withMRB_OPEN_FAILURE()(mruby.h), which is true forNULLand for a state withexcset.
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.hdid not includemruby/presym.h, so a source that did not useMRB_SYM()compiled withmruby/includealone; presym could also be turned off (MRB_NO_PRESYM). ngx_mruby 2.x compiled its own sources this way: itsconfig.inputs onlymruby/srcandmruby/includeon the include path. - Now:
mruby.hincludesmruby/presym.h, which includes the generatedmruby/presym/id.h(mruby/mruby#6733), andMRB_NO_PRESYMis gone, so every source that includesmruby.hneedsbuild/host/include. Compiled withmruby/includealone, such a source stops withfatal 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-incdirgives a list of directories without that directory. - What to do: add the
includedirectory 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-incdiris<mruby_root>/src <mruby_root>/include <mruby_root>/build/host/include(#559). A list that you give with--with-mruby-incdirneeds the directory too, because the sources of ngx_mruby includemruby.handmruby/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.rbof 2.x builds the bundled auto-ssl mrbgem (mrbgems/auto-ssl, theNginx::SSL::ACMEclasses) 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::ClientandNginx::SSL::ACME::Dehydrated; the classes and modulesAcme,CustomHttpRequest,OpenSSL,PolarSSL,HttpRequest,SimpleHttp,HTTP,Base64,Forwardable,FORWARDABLE,TempfileandTempfilePath; andDir.tmpdirandDir.mktmpdir. - Now: the default build has none of them. The constants are not defined,
Dir.tmpdirandDir.mktmpdirraiseNoMethodError, and a handler that uses one of them answers 500 (mrb_run failed: ... error: #<NameError: uninitialized constant Base64>). TheDirclass 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_ 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 ofmethod/README.md mruby_set_codeunder Nginx::Var Class callsBase64.encode, which the default build had only through auto-ssl.
- The reason: decision 6 of the v3 plan (
- Affected: scripts that use one of the constants above,
Dir.tmpdirorDir.mktmpdirinside nginx. - What to do: use
[s].pack('m0')in place ofBase64.encode(s)ands.unpack1('m')in place ofBase64.decode(s), both from the core gem mruby-pack,ENV['TMPDIR'] || '/tmp'in place ofDir.tmpdir, andDir.mkdir(path, 0700)under a name of your own in place ofDir.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 aconf.gemline in abuild_config.rbof your own; the gems that depend on pyama86/mruby-polarssl do not build with mruby 4. A server that needsNginx::SSL::ACMEkeeps 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 definesNginx::SSL::ACME::Client, a client of the ACME protocol, andNginx::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-ssland notest/conf/auto-ssl/, andbuild_config.rbno longer readsNGX_MRUBY_AUTO_SSL, so no build of this release hasNginx::SSL::ACME. A build withNGX_MRUBY_AUTO_SSL=1in its environment builds the default gems as without it. A script that usesNginx::SSL::ACMEraisesNameError, and its handler answers 500 with amrb_run failedline inerror.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 includesmruby/ext/io.h, which mruby 4.0 renamed tomruby/io.h, so the mruby build with auto-ssl stops withfatal 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
nextwould 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_ of 2.x calls auto-ssl an optional bundled mrbgem in an early phase of development and points to the scripts inmethod/README.md mrbgems/auto-ssl/mrblibfor its API. It says nothing about how long the gem is kept.
- The reason: decision 6 of the v3 plan (
- Affected: scripts that use
Nginx::SSL::ACME::ClientorNginx::SSL::ACME::Dehydrated, configurations built from the examples intest/conf/auto-ssl/, and builds that setNGX_MRUBY_AUTO_SSL=1. - What to do: get certificates from an ACME server with nginx’s own module ngx_
http_ (nginx-acme), or keep a 2.x build for a server that needsacme_ module Nginx::SSL::ACME; the documentation of 2.x describes the gem. RemoveNGX_MRUBY_AUTO_SSLfrom the environment of your builds: it has no effect.
New features
Section titled “New features”- Scripts can use the additions of mruby 3.4 and 4.0: pattern matching (
case/in),Array#rfind, trailing commas in method parameters, and&nilin 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.sqrtandModule#private_instance_methods. mruby’s lists of user-visible changes (doc/mruby3.4.md,doc/mruby4.0.md) name more (#561) Dir.childrenandDir#each_childlist the entries of a directory without.and.., as in CRuby, andDir.empty?tells whether a directory has no entries (#559, #561). ngx_mruby 2.x tookDirfrom iij/mruby-dir, which has none of the three methods.Dirnow 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 ofDir.entriesandDir#eachand 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 usingDir.entries.Diralso includesEnumerable, which iij/mruby-dir does not, soDir.open(path) { |d| d.select { |name| name.end_with?(".txt") } }works;Dir.openwith a block closes theDirwhen the block ends.
Dir.foreachwith a block closes theDirthat it opened. With iij/mruby-dir of 2.x, thatDirkept 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 logsngx_mruby: mrb_open() failedand the stream modulengx_mruby-stream-module: mrb_open() failedat theemerglevel, followed by: <class> raised while initializing mrubywhen 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)
Build and test changes
Section titled “Build and test changes”-
The installation guide integrates the corrected 2.x build/install lifecycle from #660 while selecting the
nextdevelopment 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.lockrecords 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
50031c81b101f44e89bf482f5c31213b6c07af36on the gem’smaster(branch: master): the merge commit of matsumotory/mruby-uname#1, which declares the instances ofUnameas data objects (MRB_SET_INSTANCE_TT(uname, MRB_TT_DATA)). Its tree is that of the fix commitc82f8a6, and a build no longer depends on the gem’s branchmruby-4(#561) -
The test client (the
testbuild ofbuild_config.rb) is built withNO_SSL(ENV['NO_SSL'] ||= '1'), because matsumotory/mruby-simplehttp depends on mruby-polarssl unlessNO_SSLis set, and luisbebop/mruby-polarssl depends on mruby-print.SimpleHttpandHttpRequestof the test client cannot sendhttpsrequests; the tests send HTTPS withcurl -kandopenssl s_client(docs/test/README.md). rake loads the gems of all builds in one process, soNO_SSLalso 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 inmruby/build/repos/test, about 9.8 MiB), and the build script of matsumotory/mruby-redis clones the hiredis tagv1.5.0intomruby/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
8d939104923bc86bb16d2c203d91e22bfa3196dfon the gem’smaster(branch: master): the merge commit of matsumotory/mruby-redis#109, which keeps the connection ofRedis.connect_set_udptrin themrb_stateinstead ofmrb->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 commit5bb7f030f6631b243438cf0fdd3be84352b905bband builds it withUSE_WERROR=0, and the gem declaresRedis#ttlwith one argument: mruby 4.0.0 checks the argument count of a C method against its declaration, and with the gem’s earlier declarationr.ttl "key"raisedArgumentError. The lock also pins matsumotory/mruby-pointer, which mruby-redis depends on and which adds no class, to8b46506dfdb12a79a16be1d1172ef3c9324bef96on itsmaster(#606) -
mruby/holds the tree of the mruby tag4.0.0(commit831da26b9021), merged as a subtree squash that recordsgit-subtree-split: 831da26b9021de0369d17b71b5667e2941a1a32d, so the nextupdate-mruby-subtreemerges from that commit. docs/DEVELOPMENT.md describes how to finish anupdate-mruby-subtreethat stops with conflicts undermruby/: replace the subtree with the exact tree of the ref withgit read-tree, check thatgit write-tree --prefix=mruby/andgit 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=1in the environment of the mruby build skips, with a notice, the names inNGX_MRUBY_CORE_GEMSthat 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.shbuilds ngx_mruby inbuild_soak/with-DNGX_MRUBY_DEBUG_STATS, which adds the classNginx::Debug(Nginx::Debug.gc, andNginx::Debug.statswith 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 haveNginx::Debug. Thesoakjob of CI runs the test, and onnextthe required checkci-okneeds that job (“Soak test for memory” in docs/test/README.md) (#542, #554, #557) -
The performance comparison:
sh test/perf/compare.sh BASE_DIRbuilds the checkout inBASE_DIRand this checkout inbuild_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. Theperfjob 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.rbsends requests to the agent proxy recipes oftest/conf/conf.d/60-agent-proxy.confon port 18132, with a mock LLM upstream (test/soak/mock_llm.rb) thattest/t/cases/_agent_proxy_client.rbstarts on 12372 and 12373 (“Mock LLM upstream” in docs/test/README.md) (#554) -
The mock LLM upstream
test/soak/mock_llm.rbalso answersPOST /v1/responsesas the OpenAI Responses API does, with and without a stream. A stream ends withresponse.completed,response.incompleteorresponse.failed, chosen with--terminalor the request headerx-mock-terminal. The perf lane measuresresponses_stream_plain_50in its default set andresponses_stream_plain_1000whenPERF_SCENARIOSnames it, the soak test runsagent_responses_stream, andtest/t/cases/agent_proxy.rbchecks its recipes on/v1/responsesas 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) andtest/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(areturninside a block or aforbody of handler code, 18117) (#584),test/t/cases/var_method_lookup.rb(the accessors ofNginx::Varand the methods that read variables, 18159) (#610),test/t/cases/home_example.rb(the example of the home page of the site, 18160) (#617), andtest/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(commitbd44d1630), with the one change of ngx_mruby kept: insrc/ndk_set_var.c, anNGX_DONEthat the variable handler ofmruby_setreturns becomes the status of nginx’s script engine. The tree was merged withupdate-devkit-subtree v0.3.4as a subtree squash that recordsgit-subtree-split: bd44d16302273052d6005d7bdb55f74e23813de3, so the nextupdate-devkit-subtreemerges from that commit. The tree before was the tagv0.3.1(a22dade76), whosesrc/ndk.hsaid 0.2.15. ngx_mruby enables only theset_varpart of NDK (have=NDK_SET_VARinconfig.in). Of the files that such a build compiles, the commits since v0.3.1 change onlysrc/ndk.handsrc/ndk_string_util.h, in lines that produce no machine code: the version macros ofsrc/ndk.hare used nowhere, NDK’sconfigalways definesNDK, so the removed#errornever applied, andndk_memcpypis a macro whose only user,src/ndk_conf_file.c, is not compiled. The commits fix the argument thatndk_conf_set_http_complex_path_slotpasses to its post handler and the size thatndk_memcpypcopies, correct the type in the size of the bucket array thatndk_upstream_listallocates (the number of bytes stays the same), remove the#errorthatsrc/ndk.hraised in a build withoutNDK, and setNDK_VERSIONto “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.shruns nginx-tests at a pinned commit withproveon nginx built with ngx_mruby bytest.shand 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 tobuild_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.shstops after the build and the install. Thenginx-testsworkflow of CI runs it on pull requests tonextthat change the nginx binary,test.shor the runner, and on pushes tonext; the required checkci-okdoes not need it (“nginx’s own test suite (nginx-tests)” in docs/test/README.md) (#602) -
The memory soak test also checks
Nginx::Debugitself: in each scenario oftest/soak/nginx.confit requests the new location/debug/checkonce before the warmup, and the scenario fails unlessNginx::Debug.statshas the documented keys with Integer values,Nginx::Debug.gcreturnsnil, and the full GC ofNginx::Debug.gcfrees 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 acompose.yamland aDockerfilethat build nginx with ngx_mruby from the checkout, with annginx.confand a Ruby script, and itsREADME.mdis the page/start/. Theexamplesworkflow of CI runs the commands of that README in order withexamples/check_example.pyand compares their output with what the README states, on pull requests tonextand pushes tonextthat changeexamples/, 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 checkci-okdoes not need it (#618) -
test/t/cases/agent_proxy.rbreads the values that it compares for the recipes of the Messages API fromtest/html/agent_proxy_auth.expected,agent_proxy_route.expected,agent_proxy_limit.expectedandagent_proxy_log.expected, onekey=valueper line, orkey~=textfor a value that contains the text, which the agent proxy page of the site shows;test/t/cases/_agent_proxy_client.rbtakes the port of nginx as an optional third argument (#622) -
test.shstops only the nginx that it started, by its pid. It no longer runskillall nginx(pkill nginxon 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 inlogs/nginx.pid, when that process runs thesbin/nginxof 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; afterNGINX_STOPTIMEseconds (default 60) it sends SIGKILL and exits 1. Before each signal, it checks that the pid still runs thesbin/nginxof this build, so a pid that another process took after nginx ended gets no signal.test/t/cases/_second_instance.rbsends 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 runtest.shat 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 intest/t,test/conf,test/htmland the two files oftest/soakthat the agent proxy tests load.test.shcopies these files with the new scripttest/t/cases/_stage.rbtobuild/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 methodsuite_pathoftest/t/cases/_prelude.rb. With offset 0 the copy is the same as the source._stage.rbfails 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.shprints 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, sotest.shkeeps the exit status of the tests. Before, it printed that it sent SIGKILL, andtest.shexited 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,indirectand the suppressions of the new filetest/valgrind.supp, and its step “Check the valgrind reports” fails the cell when theERROR SUMMARYof 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 thebuildmatrix (nginx 1.31.6, static module) runs nginx under valgrind, and the other cells without a sanitizer run it directly. A manual run oftest.ymlwith the new inputvalgrindruns every cell except the sanitizer cell under valgrind; the new workflowvalgrind-nightly.ymlstarts such a run every day once the file is onmaster, the default branch, and until then on pull requests that change it,test.ymlortest/valgrind.supp.test.shstill 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
deployof.github/workflows/site.ymlpublishes the site that the jobbuildbuilt and checked to GitHub Pages athttps://ngx.mruby.org/, on a push tonextand on aworkflow_dispatchonnext, never for a pull request. It usesactions/configure-pages,actions/upload-pages-artifactandactions/deploy-pages, pinned by commit, with the permissionspages: writeandid-token: writeonly, and stops unless the Pages settings give the originhttps://ngx.mruby.org. The site serves the Google verification filegoogleba3435e4002729b6.htmlof the old site at the same URL and loads no analytics script, whichsite/scripts/check-deploy.mjschecks on every run;site/scripts/check-live.mjschecks the live site after a deployment, by hand (“The site” in AGENTS.md) (#653) -
The site builds the documentation of 2.x under
/v2/fromdocs/ofmaster:node site/scripts/build-v2.mjs --tree DIR(npm run build:v2insite/), afternpm run build, writes the 2.x pages intosite/dist/v2/with a search index of their own, andsite/scripts/check-v2.mjschecks 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. Thesiteworkflow checks outmasterand builds/v2/on every run. Onmaster, thesite-v2workflow builds the site with thedocs/of every pull request tomasterand fails on a broken/v2/link, and thesite-dispatchworkflow starts thesiteworkflow onnextafter every push tomaster(#657, #656)
Upgrading
Section titled “Upgrading”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.rbof 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 removemruby/buildafter a change of the lock (#558).Hash#inspectandHash#to_s: do not use them as a data format; build the string yourself, or useJSON.generate(#561).Exception#inspect: update log parsers and alerts that match the old form of themrb_run failedlines (#561).- The messages logged while nginx reads the configuration: to see them, run
nginx -t -e stderr, read the startup log afternginx -t, or reload witherror_logatinfo(#565). NoMethodError: match on the exception class, not on the message (#561).privateandprotected: call such methods without a receiver (Integer(x)instead ofKernel.Integer(x)), or withsend; list private methods withprivate_instance_methodsorprivate_methods(#561).Module#remove_const,#private,#public,#protected,#module_functionand the hooks: call them in the body of the class or module without a receiver, or withsend(#561).- Overrides of
append_features,prepend_featuresandextend_object: move the code toincluded,prependedorextended(#561). allbits?,anybits?andnobits?on a Float: convert withto_ifirst (#561).FileTest: nothing for calls of its methods (#561).- A constant assigned in a method body: use
Object.const_set(orKernel.const_set) (#561). - Bit operations on a Float: convert with
to_ifirst, 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
Randomorsrand: do not depend on its numbers across versions (#561). Set#inspect,Set#to_sandSet#flatten_merge: do not use the first two as a data format; calls.merge(Set.new(t).flatten)in place ofs.flatten_merge(t)(#561).Hash#default_proc=: pass a Proc ornil(#561).yieldwith keyword arguments: nothing (#561).- A
returninside a block or aforbody 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 matchesresponse body is empty. return NGX_HTTP_INTERNAL_SERVER_ERRORno 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: usev.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::ServerandNginx::Request: to return other values, define the methods on those classes; read variables withNginx::Varinstead of@iv_var(#610).Dir: rescueIOErrorinstead ofRuntimeErroraround aDirthat 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 thatmrb_get_argsreads, usemruby/io.h,mrb_temp_alloc()andMRB_OPEN_FAILURE(), callmrb_open()ormrb_open_core()without an allocator and definemrb_basic_alloc_func()for a custom one, and addmruby/build/host/includeto 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')andunpack1('m')in place ofBase64,ENV['TMPDIR'] || '/tmp'in place ofDir.tmpdirandDir.mkdirin place ofDir.mktmpdir, or add the gems to abuild_config.rbof 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; removeNGX_MRUBY_AUTO_SSLfrom the build environment (#640).