Migrating from ngx_mruby 2.x to 3.0.0
This guide is for moving scripts, configurations and builds from ngx_mruby 2.x to ngx_mruby 3.0.0. Each change that it covers is described once, under Behavior changes: read before upgrading in the release notes of v3.0.0: what 2.x did, what 3.0.0 does, what is affected and what to do. This guide links to each of those entries and adds the steps and examples: how to find the scripts and builds that an entry affects, what a change looks like in a script, and how to check the result. Read the release notes of the 2.x releases newer than the version you run first (docs/releases).
ngx_mruby 2.x bundles mruby 3.3.0. The development line of ngx_mruby 3
bundles mruby 4.0.0, the current stable release of mruby. The v3 plan
(docs/proposals/v3-plan.md, section 7, decision 1) sets mruby 4.1.0 as
the target of v3, with 4.0.0 as an approved step before it; 4.1.0 has only
release candidates so far. The entries of mruby 4.0.0 are the changes that
were found and measured for ngx_mruby, not a complete list. mruby’s own
lists of user-visible changes are doc/mruby3.4.md and doc/mruby4.0.md
in mruby/. mruby 4.1.0 brings further changes that the release notes and
this guide do not cover yet.
- Check how you build the module: Builds.
- Find the scripts that an entry affects, and change them: Scripts.
- Update log parsers and alerts: Log monitoring.
- If you maintain an mrbgem or other C code that uses the mruby C API, check it: C extensions and mrbgems.
- Check the result: Checking the result.
Builds
Section titled “Builds”-
With the
build_config.rbof this release, rebuild the module; the bundled mruby is built with it (The bundled mruby is 4.0.0 instead of 3.3.0). -
With a
build_config.rbof your own (Abuild_config.rbof your own needs the core gem changes of mruby 4.0.0), find the lines that select core gems:grep -nE 'gembox|mruby-print|hal-posix|mruby-task|mruby-encoding|mruby-sleep|ngx_mruby_core_gems' build_config.rbTo keep the core gems that the default build of ngx_mruby takes, copy
NGX_MRUBY_CORE_GEMSandngx_mruby_core_gemsfrom thebuild_config.rbof this release, and callngx_mruby_core_gems(conf)in place ofconf.gembox 'full-core', as that file does. A build that needs fewer gems passesNGX_MRUBY_CORE_GEMS - %w[...]. To add a gem that the list leaves out, follow Core gems. -
A build from a new checkout or a source archive gets the third-party mrbgems at the commits that
build_config.rb.lockrecords (The third-party mrbgems are built at the commits thatbuild_config.rb.lockrecords). In a tree that keepsmruby/build, remove it after a change of the lock, before the next build; otherwise rake stops withfatal: reference is not a treewhen a clone does not have the new commit:rm -rf mruby/buildTo build a gem at another commit, follow Gem commits.
-
The default build no longer has the auto-ssl mrbgem and the classes that came with it (The default build leaves out the auto-ssl mrbgem and the classes that came with it). Scripts says how to find the scripts that use them.
-
No build of this release has the auto-ssl mrbgem, and the mruby build no longer reads
NGX_MRUBY_AUTO_SSL(The auto-ssl mrbgem andNGX_MRUBY_AUTO_SSLare removed). Remove the variable from the environment of your builds; it has no effect. A server that gets its certificates withNginx::SSL::ACMEmoves to nginx’s ngx_http_ (nginx-acme) before the upgrade, or stays on 2.x. This finds the builds and the configurations that use them:acme_ module grep -rnE 'NGX_MRUBY_AUTO_SSL|auto-ssl|Nginx::SSL::ACME' /path/to/build/scripts /path/to/nginx/conf -
With
./configure --with-mruby-incdir, add<mruby_root>/build/host/includeto the list of directories (Sources that includemruby.hneed the generated headers). -
With mrbgems of your own or of third parties, see C extensions and mrbgems.
Scripts
Section titled “Scripts”Most entries name a method that you can search for. This finds them in the
script files of a configuration (replace /path/to/scripts with the
directories of the files that the *_handler directives name):
grep -rnE 'Kernel\.|valid_encoding\?|allbits\?|anybits\?|nobits\?|remove_const|module_function|append_features|prepend_features|extend_object|singleton_method_added|\.(included|extended|prepended|inherited|method_added)\(|initialize_copy|respond_to_missing|respond_to\?|method_defined\?|method_missing|instance_methods|\.methods|iv_var|default_proc *=|FileTest|srand|Random\.new|Set\.new|Set\[|flatten_merge|RuntimeError' /path/to/scriptsSearch the inline code of the *_code directives in the nginx.conf
files the same way. The items below say what to look for where a name is
not enough, and show the change.
-
Hash#inspectandHash#to_suse a new format: look forinspect,to_s,"#{...}"andsprintforformatwith%swhere the value can be a Hash or an Array of Hashes, and for=>in the strings that a script or a test compares with.h = {"q" => "x"}Nginx.rputs h.to_s # 2.x: {"q"=>"x"} 3.0.0: {"q" => "x"}Nginx.rputs JSON.generate(h) # {"q":"x"} on both -
Set#inspectandSet#to_suse a new format, andSet#flatten_mergeis removed: the same search for a Set;set.to_a.join(",")gives the same string on both versions. The search above also findsflatten_merge:s.flatten_merge(t)becomess.merge(Set.new(t).flatten), which gives on both versions the Set thats.flatten_merge(t)gave on 2.x, whentis a Set or an Array.s.merge(t.flatten)gives that Set only whentis a Set: for an Arrayt,Array#flattenflattens the Arrays inside it and keeps the Sets, whereflatten_mergedid the opposite:t = [2, Set.new([3])]Set.new([1]).flatten_merge(t) # 2.x: the Set of 1, 2 and 3 3.0.0: NoMethodErrorSet.new([1]).merge(Set.new(t).flatten) # the Set of 1, 2 and 3 on bothSet.new([1]).merge(t.flatten) # the Set of 1, 2 and a Set that holds 3, on both -
The message of
NoMethodErrornames the class of the receiver: look formessagenext torescue NoMethodError, and forundefined methodin scripts.e.message == "undefined method 'foo'" # 2.x: true 3.0.0: false (the message goes on with " for NilClass")Rescue
NoMethodErroritself instead of comparing the message. -
privateandprotectedare enforced: look for calls, with a receiver other thanself, ofinitialize,initialize_copy,respond_to_missing?andmethod_missing, of methods defined withdefat the top level, ofKernelmethods such asraise,sprintfandputs, and of methods belowprivateorprotected. In handler code, a method defined withdefat the top level stays public, and calls of it with a receiver need no change (the proposal for #502, section 1.4). The search above finds the calls onKernel(Kernel\.): onlyKernel.format,.sprintf,.raise,.sleepand.usleepstill work, and the others, such asKernel.Integer(x), become calls without a receiver. It also findsinstance_methodsandmethods, which leave private methods out. Checkingrespond_to?before such a call does not help: it returnstruefor a private method.class Routerdef call(r)pick(r) # no receiver: works on bothendprivatedef pick(r)r.uriendendRouter.new.pick(r) # 2.x: works 3.0.0: NoMethodErrorRouter.new.send(:pick, r) # works on bothKernel.Integer("12") # 2.x: 12 3.0.0: NoMethodError (private method 'Integer' called for Module)Integer("12") # 12 on both -
Module#remove_const,#private,#public,#protected,#module_functionand the hooks are private: the search above finds most of them; look also for.private(,.public(and.protected(called on a class or module. For example,Foo.remove_const(:X)becomesFoo.send(:remove_const, :X). -
Overrides of
append_features,prepend_featuresandextend_objectare not called: the search above finds them.module Plugindef self.append_features(base) # 3.0.0: never called, and no errorbase.extend(ClassMethods)superendendmodule Plugindef self.included(base) # called on bothbase.extend(ClassMethods)endend -
allbits?,anybits?andnobits?are methods ofIntegeronly: the search above finds them;x.allbits?(1)on a Float becomesx.to_i.allbits?(1). -
FileTestis a module: the search above findsFileTest; only uses other than calls of its methods (FileTest.exist?and so on) need a change. -
Assigning a constant in a method body is a
SyntaxError: compiling the scripts finds it (see Checking the result).def setupTable = {} # 3.0.0: SyntaxError when the script is compiledObject.const_set(:Table, {}) # works on bothend -
Float values have no bit operations: look for
<<,>>,&,|,^and~applied to a value that can be a Float: the result ofto_f, ofFloat()or of arithmetic with a Float. The entry says how to get the number that 2.x gave.x = 1.5x << 1 # 2.x: 3 3.0.0: NoMethodError(x * 2).to_i # 3, the number of 2.x for x << 1x.to_i << 1 # 2, another number -
String#valid_encoding?is not in the default build: the search above finds it. With the defaultbuild_config.rbof 2.x the method returnedtruefor every string, so a check like this one never rejected a request and can be removed:Nginx.return Nginx::HTTP_BAD_REQUEST unless s.valid_encoding? -
A seeded
Randomgives other numbers: the search above findssrandandRandom.new; look also for tests that expect the numbers of a seeded generator. -
Hash#default_proc=raisesTypeErrorfor an object that is not a Proc: the search above finds it; pass a Proc, for exampleh.default_proc = proc { |hash, key| hash[key] = [] }. -
yieldwith keyword arguments to a block with keyword parameters works: nothing to change; arescue ArgumentErrorthat was there for this case is no longer reached. -
A
returninside a block or aforbody of handler code ends the code: nothing to change in the scripts. Areturninside a block or the body offorat the top level of handler code no longer raisesLocalJumpError: the handler answers with the response that its code set before thereturn, where 2.x answered 500 (2.7.0 answered 200 with the output that a content handler had written before it). Log alerts onunexpected return (LocalJumpError)no longer fire for it. -
A content handler that ends without output answers 200 with an empty body instead of 500: look at the
mruby_content_handlerandmruby_content_handler_codehandlers that can end without writing a body: a handler that writes only when a condition holds, one that passes a value that can be""orniltoNginx.rputs, one that only sets response headers, and one that ends withNginx.return Nginx::HTTP_OKor another status from 200 to 299 other than 201 and 204. On 3.0.0 they answer that status (200 when none is set) withContent-Length: 0; on 2.7.1 they answered 500. A handler that relied on the 500 sets the status that it means:body = Nginx::Request.new.headers_in["X-Answer"].to_sNginx.rputs body # body "": 2.7.1 answers 500 3.0.0 answers 200 with Content-Length: 0if body.empty?Nginx.return Nginx::HTTP_NOT_FOUND # 404 on bothelseNginx.rputs bodyend -
An output body filter that ends with an empty body sends the response instead of closing the connection: nothing to change. A body filter that sets
""withNginx::Filter#body=or#output, or leaves the body of a response without a body empty, sends the status withContent-Length: 0, where 2.x closed the connection without a byte. -
The accessors of
Nginx::Varbecome methods ofNginx::Varat their first use in a worker: the search above findsrespond_to?,method_defined?,instance_methodsandmethods; look at the ones on anNginx::Varor onNginx::Varitself. Their answer for a variable now depends on whether the worker has served an access of the name before. Useexist?instead:v = Nginx::Var.newv.respond_to?(:request_method) # 2.x: false 3.0.0: true after the first v.request_method in the workerv.exist?("request_method") # true on bothAlso look for methods that the scripts define on
ObjectorKernel, for example with adefat the top level of handler code, whose name is also the name of a variable that a script reads withv.NAME. Once the accessor exists in a worker,v.NAMEreads the variable instead of calling such a method. Rename the method. The search also findsmethod_missing: a script that wrapsNginx::Var#method_missing, for example with a module prepended toNginx::Varwhosemethod_missingcallssuper, sees only the first access of a name that gets an accessor. Move what the wrapper does to the code that reads the variable. -
Nginx::Connection,Nginx::ServerandNginx::Requestread their variables without callingNginx::Var: look for methods that a script defines onNginx::Var(class Nginx::Var,Nginx::Var.class_eval,define_method) and foriv_var, which the search above finds. A method ofNginx::Varno longer changes whatNginx::Connection#remote_ipand the other methods of the entry return; define the method on their class instead, inmruby_init_code, which runs once when nginx reads the configuration. Handler code runs on every request in the samemrb_state, so there the secondalias_methodwould makenginx_remote_ipa copy of the newremote_ip, which then calls itself:mruby_init_code 'class Nginx::Connectionalias_method :nginx_remote_ip, :remote_ipdef remote_ipNginx::Request.new.headers_in["X-Real-IP"] || nginx_remote_ip # works on bothendend'; -
Dircomes from mruby’s core gem, and a closedDirraisesIOErrorinstead ofRuntimeError: the search above findsRuntimeError; look at the ones that rescue an exception of aDirthat can be closed, or ofDir.openwith a block that closes theDir.d = Dir.open(path)d.closebegind.readrescue RuntimeError # 2.x: rescued 3.0.0: not rescued (IOError)endbegind.readrescue IOError, RuntimeError # rescued on bothend -
The default build leaves out the auto-ssl mrbgem and the classes that came with it: this finds the constants and the two
Dirmethods that the default build no longer has:grep -rnE 'Nginx::SSL::ACME|Acme|HttpRequest|OpenSSL|PolarSSL|SimpleHttp|(^|[^:])HTTP::|Base64|Forwardable|FORWARDABLE|Tempfile|tmpdir' /path/to/scriptsBase64,Dir.tmpdirandDir.mktmpdirhave replacements in the default build:Base64.encode(s) # 3.0.0: NameError (uninitialized constant Base64)[s].pack('m0') # the same string on bothBase64.decode(b) # 3.0.0: NameErrorb.unpack1('m') # the same string on bothDir.tmpdir # 3.0.0: NoMethodErrorENV['TMPDIR'] || '/tmp' # works on bothFor
Dir.mktmpdir, see The auto-ssl mrbgem and the classes that came with it. The other constants need aconf.gemline of your own.
Dir.children, Dir#each_child, Dir.empty? and the methods of
Enumerable on a Dir are new to scripts of 2.x; the first two leave out
. and .. (see New features).
Log monitoring
Section titled “Log monitoring”-
Exception#inspect, and so themrb_run failedlines oferror.log, use a new format: look in log parsers and alert rules for patterns onmrb_run failedlines, and for an exception class in parentheses at the end of a line.2.x: mrb_run failed: return 500 HTTP status code to client: error: boom (RuntimeError)3.0.0: mrb_run failed: return 500 HTTP status code to client: error: #<RuntimeError: boom>A pattern such as
\(RuntimeError\)$becomes#<RuntimeError:. -
The message of
NoMethodErrornames the class of the receiver: a pattern that ends atundefined method 'foo'no longer matches, because the message goes on withfor <class>. -
The messages logged while nginx reads the configuration are at the
infolevel instead ofnotice: look in log parsers, alert rules and start scripts formechanism enabled,compile info,target variableandngx_mruby NOTICE. A start of nginx no longer writes these lines.nginx -twrites them at[info]to the startup log, which is the file given with-e, or else the error log path compiled into nginx (logs/error.logunder the prefix by default). nginx copies a line of the startup log to the terminal only atwarnand above, so the output ofnginx -tdoes not show them.2.x, start: [notice] ... ngx_mruby/<version> (mruby/3.3.0) mechanism enabled in ...3.0.0, start: no such line3.0.0, nginx -t: [info] ... ngx_mruby/<version> (mruby/4.0.0) mechanism enabled in ... (startup log only)3.0.0, nginx -t -e stderr: [info] ... ngx_mruby/<version> (mruby/4.0.0) mechanism enabled in ... (terminal)A check that looks for one of these lines to see that the module is loaded runs
nginx -t -e stderrand reads its output, or reads the startup log afternginx -t:nginx -t -e stderr 2>&1 | grep 'mechanism enabled' -
A content handler that ends without output answers 200 with an empty body instead of 500: a pattern on
response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERRORno longer matches for the content handlers of the main request that end without output and with no status or a status from 200 to 299 that nginx does not answer by itself; it still matches for a status below 200, and forNginx::HTTP_OKwithout output in the other handlers and in subrequests. -
An output body filter that ends with an empty body sends the response instead of closing the connection: an alert on
zero size buf in writerno longer fires for these responses, because nginx sends them.
C extensions and mrbgems
Section titled “C extensions and mrbgems”This finds the places in the C sources of a gem, or of a program that embeds mruby, that the entries below are about:
grep -rnE 'Data_Wrap_Struct|mrb_data_object_alloc|MRB_SET_INSTANCE_TT|MRB_ARGS_|mruby/ext/io\.h|mrb_alloca|mrb_open|allocf|MRB_NO_PRESYM' src/-
An object of a class not declared
MRB_TT_DATAcannot wrap C data: every class whose objectsData_Wrap_Structormrb_data_object_alloccreates needs the declaration aftermrb_define_class:struct RClass *c = mrb_define_class(mrb, "Uname", mrb->object_class);MRB_SET_INSTANCE_TT(c, MRB_TT_DATA); -
mruby checks the argument count of a C method against its spec: compare the spec of each
mrb_define_methodwith what itsmrb_get_argsreads, in both directions. A method defined withMRB_ARGS_REQ(2)that reads one string (mrb_get_args(mrb, "S", &s)) and is called with one argument raisesArgumentError; define it withMRB_ARGS_REQ(1). A method defined withMRB_ARGS_REQ(1)that reads"o|o"and is called with two arguments raises too; define it withMRB_ARGS_ARG(1, 1). -
Renamed headers and functions, and
mrb_open()on failure:#include <mruby/io.h> /* was <mruby/ext/io.h> */void *buf = mrb_temp_alloc(mrb, size); /* was mrb_alloca(mrb, size) */mrb_state *mrb = mrb_open(); /* also in place of mrb_open_allocf(my_allocf, ud) */if (MRB_OPEN_FAILURE(mrb)) { /* ... */ } /* was: if (mrb == NULL) *//* a custom allocator, in place of mrb_default_allocf() or my_allocf */void *mrb_basic_alloc_func(void *ptr, size_t size) { /* ... */ } -
Sources that include
mruby.hneed the generated headers: add-I<mruby>/build/host/includenext to-I<mruby>/includewhere you compile such a source outside the mruby build.
Checking the result
Section titled “Checking the result”-
Compile every script with the
mrbcof mruby 4.0.0. After a build of this release (sh build.shorsh test.sh),mruby/bin/mrbc -cchecks the syntax of a script: it printsSyntax OK, or the line of the error (for a constant assigned in a method body,<file>:2:3: dynamic constant assignment) and exits with 1.for f in /path/to/scripts/*.rb; do mruby/bin/mrbc -c "$f" || echo "check: $f"; done -
Run the parts of a script that do not call the
Nginxclasses with both versions of mruby. The entries of mruby 4.0.0 were measured this way between 2026-10-04 and 2026-10-14: the same Ruby script ran with twomrubybinaries, one built from the mruby tree of ngx_mruby 2.x (mruby 3.3.0) and one from mruby 4.0.0, in Docker (Ubuntu 22.04, gcc 11), with mruby’sdefaultgembox or with the core gems of each version’s ngx_mruby build (full-coreand iij/mruby-dir for 3.3.0,NGX_MRUBY_CORE_GEMSfor 4.0.0). The entries that say “the test suite of ngx_mruby saw the change” also changed a response or a log line of ngx_mruby itself. To do the same, copy themruby/directory of each version to a directory of its own, runrake MRUBY_CONFIG=defaultin each copy, and run your script with thebin/mrubyof both copies. Do not run that rake in themruby/of an ngx_mruby checkout: it builds intomruby/build, where the build of the module keeps its mruby. -
Run the configuration with the module of this release on a test server, send the requests that reach each handler, and look in
error.logformrb_run failedlines withNoMethodError,TypeError,ArgumentErrororSyntaxError.
These were checked the same way and did not change, so they need no check:
String#size and #length count bytes (the mruby-encoding gem is not in
the default build; see Core gems); the
ArgumentError for a wrong number of arguments; Exception#message;
Struct#inspect; Float#to_s; Integer division and modulo; Kernel#sleep
and #usleep (from mruby-sleep).