---
title: "ngx_mruby v3.0.0"
language: "en"
documentation_version: "3.x"
channel: "development"
release_context: null
source_revision: "10297d1e28653321d6996609fc53718d3b7c8278"
working_tree_clean: true
source_path: "docs/releases/v3.0.0.md"
source_sha256: "fae8ea9cd5637619e7020436915de16bfcb3248ce2d56acacf87340903c4f52b"
source_at_revision: true
source_url: "https://github.com/matsumotory/ngx_mruby/blob/10297d1e28653321d6996609fc53718d3b7c8278/docs/releases/v3.0.0.md"
html_url: "https://ngx.mruby.org/releases/v3.0.0/"
markdown_url: "https://ngx.mruby.org/markdown/releases/v3.0.0.md"
last_updated: "2026-10-30"
---

# ngx_mruby v3.0.0

## Behavior changes: read before upgrading

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

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

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

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

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

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

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

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

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

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

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

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

### 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)

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

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

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

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

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

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

- Before: methods of `Numeric`, so they work on a Float (`1.5.allbits?(1)` is `true`).
- Now: methods of `Integer` only; on a Float they raise `NoMethodError`. On an Integer they work as before (`6.allbits?(2)` is `true`).
- Affected: scripts that call them on a Float.
- What to do: convert a Float with `to_i` first.

### `FileTest` is a module (#561)

- Before: `FileTest` is a class.
- Now: `FileTest` is a module. `FileTest.exist?` and its other methods are called as before.
- Affected: scripts that use `FileTest` as a class, other than by calling its methods.
- What to do: nothing for calls of its methods.

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

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

### Float values have no bit operations (#561)

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

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

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

### A seeded `Random` gives other numbers (#561)

- Before: `Random.new(seed)` and `srand(seed)` give one sequence for a seed (xoshiro).
- Now: another sequence for the same seed: mruby 4.0 uses PCG instead of xoshiro.
- Affected: scripts that depend on the numbers of a seeded generator.
- What to do: do not depend on the numbers of a seeded generator across versions.

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

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

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

- 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)

- 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)

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

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

- Before: on 2.7.1 and 2.7.2, a content handler on the main request that ended without output (it wrote nothing, or only `""` or `nil` with `Nginx.rputs`, or only set response headers) got 500 with nginx's error page (or the `error_page` configured for 500) and `Connection: close` when it set no status, `Nginx::HTTP_OK` (with `Nginx.return`, `Nginx.send_header` or `Nginx.status_code=`) or another status below 300 other than 201 and 204 (for example `Nginx::HTTP_ACCEPTED`), also after `Nginx::Async::HTTP.sub_request` resumed it ([entry 13 of the 2.7.1 release notes](/releases/v2.7.1/#13-a-content-handler-that-ends-without-output-answers-500)). error.log got a line at the `error` level: `status code is 200, but response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERROR`, `status code is not set, and response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERROR` or `status code is <N>, but response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERROR`. On 2.7.0 and earlier, `Nginx::HTTP_OK` without output got the same 500, and the other cases got no response at all: nginx closed an HTTP/1.0 connection without sending a byte and kept a keep-alive connection open without a response until `keepalive_timeout`.
- Now: such a handler answers as nginx's own `return 200;` does: with the status it set, 200 when it set none, `Content-Length: 0` and an empty body. It sends no `Content-Type`, as ngx_mruby's answers with output do not; nginx's `return 200;` sends the one of `default_type`. A HEAD request gets the same header. The header filters and body filters of the location (`mruby_output_header_filter`, `mruby_output_body_filter` and their `_code` forms) run on the empty response. The empty answer discards an unread request body first, as the answer with output does since 2.7.1 ([entry 12 of the 2.7.1 release notes](/releases/v2.7.1/#12-an-unread-request-body-is-discarded-before-the-answer)), 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](https://github.com/matsumotory/ngx_mruby/issues/200): a content handler that ended with `Nginx.return Nginx::HTTP_OK` and no body answered 500, where nginx's own `return 200;` answers 200 with an empty body. HTTP allows that response: "a 200 response is expected to contain message content unless the message framing explicitly indicates that the content has zero length" ([RFC 9110, section 15.3.1](https://www.rfc-editor.org/rfc/rfc9110#section-15.3.1)), which `Content-Length: 0` does. 2.7.1 gave the same 500 to no status and to the other statuses below 300 that nginx does not answer by itself, because those requests had got no response at all ([#566](https://github.com/matsumotory/ngx_mruby/pull/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](https://github.com/matsumotory/ngx_mruby/blob/next/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](https://github.com/matsumotory/ngx_mruby/blob/v2.7.1/docs/class_and_method/README.md#nginxreturnnginx_status_code)). Before 2.7.1, it said only that `Nginx.return` "Returns nginx status code to send back HTTP status to the client" ([Nginx.return of 2.7.0](https://github.com/matsumotory/ngx_mruby/blob/v2.7.0/docs/class_and_method/README.md#nginxreturnnginx_status_code)).
- Affected: `mruby_content_handler` and `mruby_content_handler_code` handlers that can end without output, for example a handler that writes only when a condition holds, one that passes a value that can be empty or `nil` to `Nginx.rputs`, one that only sets response headers, and one that ends with `Nginx.return Nginx::HTTP_OK` and no body, and log monitoring of the lines above. Unchanged: a status below 200 without output (500 and the `status code is <N>, but response body is empty` line, as on 2.7.1 and 2.7.2), 201, 204 and the statuses from 300 up (nginx answers them), handlers with output, `Nginx.redirect`, the other handlers (`Nginx.return Nginx::HTTP_OK` without output in a rewrite or access handler still answers 500 and logs `status code is 200, but response body is empty`), and subrequests (`Nginx::Async::HTTP.sub_request`, SSI includes and `auth_request`): the content handler of a subrequest that returns `Nginx::HTTP_OK` without output still sets the subrequest's status to 500, and one that writes nothing and sets no status still ends the subrequest without a response body and without a status.
- What to do: nothing for most configurations: a handler that should answer with an empty body may now return `Nginx::HTTP_OK`. A script that relied on the 500 to signal that it had nothing to send must set that status itself, for example `Nginx.return Nginx::HTTP_INTERNAL_SERVER_ERROR` or `Nginx.return Nginx::HTTP_NOT_FOUND`, which nginx answers as before. Log monitoring that matches `response body is empty. return NGX_HTTP_INTERNAL_SERVER_ERROR` finds only the statuses below 200 of content handlers, and `Nginx::HTTP_OK` in the other handlers and in subrequests.
- Tests: port 18136 (`test/conf/conf.d/54-content-empty-body.conf`, `test/t/cases/content_empty_body.rb`, with two `POST` requests whose body the handler does not read on one connection), and the cases "Nginx.return 200 dont have body" of `test/t/ngx_mruby.rb` and "send_header 200 without a body answers 200 with an empty body" of `test/t/cases/request_api.rb`.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

- Before: ngx_mruby 2.x bundles the auto-ssl mrbgem (`mrbgems/auto-ssl`) and takes it in the default build. It defines `Nginx::SSL::ACME::Client`, a client of the ACME protocol, and `Nginx::SSL::ACME::Dehydrated`, helpers for the dehydrated ACME client; `test/conf/auto-ssl/` has example configurations for both.
- Now: this release has no `mrbgems/auto-ssl` and no `test/conf/auto-ssl/`, and `build_config.rb` no longer reads `NGX_MRUBY_AUTO_SSL`, so no build of this release has `Nginx::SSL::ACME`. A build with `NGX_MRUBY_AUTO_SSL=1` in its environment builds the default gems as without it. A script that uses `Nginx::SSL::ACME` raises `NameError`, and its handler answers 500 with a `mrb_run failed` line in `error.log`.
- Why it changed:
  - The reason: decision 6 of the v3 plan (`docs/proposals/v3-plan.md`, section 7) replaces auto-ssl with nginx-acme and with examples that look certificates up in a store, and the gem cannot work with the bundled mruby. It needs pyama86/mruby-polarssl for TLS, a gem that ngx_mruby does not maintain and that has had no commit since 2017-03-03. That gem includes `mruby/ext/io.h`, which mruby 4.0 renamed to `mruby/io.h`, so the mruby build with auto-ssl stops with `fatal error: mruby/ext/io.h: No such file or directory` (#561). Its ACME client, pyama86/mruby-acme-client, speaks ACMEv1, which Let's Encrypt turned off in 2021-06 (sections 2.2 and 3.4 of the plan).
  - Why the old behavior could not be kept: the gem cannot be built with mruby 4 until pyama86/mruby-polarssl changes, and even built it could not get a certificate from Let's Encrypt. Keeping it on `next` would have kept a gem that no build of v3 can use, with an opt-in variable that ends in a stopped build. ngx_mruby 2.x keeps the gem.
  - What the documentation promised: [docs/class_and_method/README.md](https://github.com/matsumotory/ngx_mruby/blob/v2.7.0/docs/class_and_method/README.md#auto-ssl-mrbgem) of 2.x calls auto-ssl an optional bundled mrbgem in an early phase of development and points to the scripts in `mrbgems/auto-ssl/mrblib` for its API. It says nothing about how long the gem is kept.
- Affected: scripts that use `Nginx::SSL::ACME::Client` or `Nginx::SSL::ACME::Dehydrated`, configurations built from the examples in `test/conf/auto-ssl/`, and builds that set `NGX_MRUBY_AUTO_SSL=1`.
- What to do: get certificates from an ACME server with nginx's own module [ngx_http_acme_module](https://nginx.org/en/docs/http/ngx_http_acme_module.html) (nginx-acme), or keep a 2.x build for a server that needs `Nginx::SSL::ACME`; the [documentation of 2.x](https://github.com/matsumotory/ngx_mruby/blob/v2.7.2/docs/class_and_method/README.md#auto-ssl-mrbgem) describes the gem. Remove `NGX_MRUBY_AUTO_SSL` from the environment of your builds: it has no effect.

## 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 `&nil` in method parameters (#561)
- Scripts can call methods that mruby 3.3.0 does not have, for example `Kernel#public_send`, `String#b`, `Hash#slice!`, `Array#fetch_values`, `Integer#bit_length`, `Integer.sqrt` and `Module#private_instance_methods`. mruby's lists of user-visible changes ([`doc/mruby3.4.md`](https://github.com/mruby/mruby/blob/4.0.0/doc/mruby3.4.md), [`doc/mruby4.0.md`](https://github.com/mruby/mruby/blob/4.0.0/doc/mruby4.0.md)) name more (#561)
- `Dir.children` and `Dir#each_child` list the entries of a directory without `.` and `..`, as in CRuby, and `Dir.empty?` tells whether a directory has no entries (#559, #561). ngx_mruby 2.x took `Dir` from iij/mruby-dir, which has none of the three methods. `Dir` now comes from mruby's core gem mruby-dir (see [Class and Method](/reference/ruby/)), and with mruby 4.0.0 the two methods leave out `.` and `..` (since mruby 3.4, mruby/mruby#6265). With mruby 3.3.0, which no release of ngx_mruby combined with mruby-dir, they were aliases of `Dir.entries` and `Dir#each` and included `.` and `..`; the test suite of ngx_mruby saw the change (`test/t/cases/gem_dir.rb`). A script written for such a build that skipped `.` and `..` after these calls can drop that code, or keep using `Dir.entries`. `Dir` also includes `Enumerable`, which iij/mruby-dir does not, so `Dir.open(path) { |d| d.select { |name| name.end_with?(".txt") } }` works; `Dir.open` with a block closes the `Dir` when the block ends.

## Fixes

- `Dir.foreach` with a block closes the `Dir` that it opened. With iij/mruby-dir of 2.x, that `Dir` kept a file descriptor of the worker open until the GC freed it (#559)
- When `mrb_open()` fails while nginx reads the configuration, the http module logs `ngx_mruby: mrb_open() failed` and the stream module `ngx_mruby-stream-module: mrb_open() failed` at the `emerg` level, followed by `: <class> raised while initializing mruby` when the initialization of mruby or of a gem raised an exception. nginx stops as before; ngx_mruby 2.x logged no line of its own (#559)

## Build and test changes

- The installation guide integrates the corrected 2.x build/install lifecycle from #660 while selecting the `next` development branch, retaining v3 core-gem and lock-file contracts. English/Japanese examples use the same commands; a fresh development build and loopback HTTP trial are recorded with their revision and limits.

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

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

- `build_config.rb.lock` records mruby 4.0.0 (`version: 4.0.0`, `release_no: 40000`, written by rake) and no longer has an entry for luisbebop/mruby-polarssl, which the test build no longer clones (#561)
- The lock pins matsumotory/mruby-uname, in the host and the test build, to `50031c81b101f44e89bf482f5c31213b6c07af36` on the gem's `master` (`branch: master`): the merge commit of matsumotory/mruby-uname#1, which declares the instances of `Uname` as data objects (`MRB_SET_INSTANCE_TT(uname, MRB_TT_DATA)`). Its tree is that of the fix commit `c82f8a6`, and a build no longer depends on the gem's branch `mruby-4` (#561)
- The test client (the `test` build of `build_config.rb`) is built with `NO_SSL` (`ENV['NO_SSL'] ||= '1'`), because matsumotory/mruby-simplehttp depends on mruby-polarssl unless `NO_SSL` is set, and luisbebop/mruby-polarssl depends on mruby-print. `SimpleHttp` and `HttpRequest` of the test client cannot send `https` requests; the tests send HTTPS with `curl -k` and `openssl s_client` ([docs/test/README.md](/contributing/test/)). rake loads the gems of all builds in one process, so `NO_SSL` also reaches a host build of your own that has mruby-simplehttp (#561)
- The default build makes 21 clones (14 in `mruby/build/repos/host`, 7 in `mruby/build/repos/test`, about 9.8 MiB), and the build script of matsumotory/mruby-redis clones the hiredis tag `v1.5.0` into `mruby/build/host/mrbgems/mruby-redis/hiredis-v1.5.0`; luisbebop/mruby-polarssl with its submodule ARMmbed/mbedtls and mruby/mgem-list are no longer cloned (#561, #606)
- The default build includes matsumotory/mruby-redis again, as the default build of 2.7.2 does ("Redis is in the default build again" in the 2.7.2 notes, #604). The lock pins it, in the host build, to `8d939104923bc86bb16d2c203d91e22bfa3196df` on the gem's `master` (`branch: master`): the merge commit of matsumotory/mruby-redis#109, which keeps the connection of `Redis.connect_set_udptr` in the `mrb_state` instead of `mrb->ud` ("Redis.connect_set_udptr and Redis.new without arguments keep the connection in the mrb_state" in the 2.7.2 notes). From matsumotory/mruby-redis#108, the build of that commit clones the hiredis tag `v1.5.0`, checks its commit `5bb7f030f6631b243438cf0fdd3be84352b905bb` and builds it with `USE_WERROR=0`, and the gem declares `Redis#ttl` with one argument: mruby 4.0.0 checks the argument count of a C method against its declaration, and with the gem's earlier declaration `r.ttl "key"` raised `ArgumentError`. The lock also pins matsumotory/mruby-pointer, which mruby-redis depends on and which adds no class, to `8b46506dfdb12a79a16be1d1172ef3c9324bef96` on its `master` (#606)
- `mruby/` holds the tree of the mruby tag `4.0.0` (commit `831da26b9021`), merged as a subtree squash that records `git-subtree-split: 831da26b9021de0369d17b71b5667e2941a1a32d`, so the next `update-mruby-subtree` merges from that commit. [docs/DEVELOPMENT.md](/contributing/development/) describes how to finish an `update-mruby-subtree` that stops with conflicts under `mruby/`: replace the subtree with the exact tree of the ref with `git read-tree`, check that `git write-tree --prefix=mruby/` and `git rev-parse REF^{tree}` print the same id, and commit the merge alone, with the build changes in the commits that follow (#561)
- `NGX_MRUBY_ALLOW_MISSING_CORE_GEMS=1` in the environment of the mruby build skips, with a notice, the names in `NGX_MRUBY_CORE_GEMS` that the mruby being built does not have, where the build would otherwise stop. It is meant for the time of an update of the bundled mruby ([docs/DEVELOPMENT.md](/contributing/development/)) (#559)
- The memory soak test: `sh test/soak/run.sh` builds ngx_mruby in `build_soak/` with `-DNGX_MRUBY_DEBUG_STATS`, which adds the class `Nginx::Debug` (`Nginx::Debug.gc`, and `Nginx::Debug.stats` with the GC registrations that ngx_mruby counts), and runs its scenarios against it. It listens on 12360 and 12361, and starts a mock LLM upstream on 12362 for the agent proxy scenarios. The default builds of build.sh and test.sh do not have `Nginx::Debug`. The `soak` job of CI runs the test, and on `next` the required check `ci-ok` needs that job ("Soak test for memory" in [docs/test/README.md](/contributing/test/)) (#542, #554, #557)
- The performance comparison: `sh test/perf/compare.sh BASE_DIR` builds the checkout in `BASE_DIR` and this checkout in `build_perf/`, and compares the instructions per request that callgrind counts in each scenario. It listens on 12370 and 12371, and starts the mock LLM upstream on 12372. The `perf` job of CI runs it for pull requests that change an input of the measured binary, the measurement or the workflow ("Performance comparison with callgrind" in [docs/test/README.md](/contributing/test/)) (#549, #554)
- The agent proxy tests: `test/t/cases/agent_proxy.rb` sends requests to the agent proxy recipes of `test/conf/conf.d/60-agent-proxy.conf` on port 18132, with a mock LLM upstream (`test/soak/mock_llm.rb`) that `test/t/cases/_agent_proxy_client.rb` starts on 12372 and 12373 ("Mock LLM upstream" in [docs/test/README.md](/contributing/test/)) (#554)
- The mock LLM upstream `test/soak/mock_llm.rb` also answers `POST /v1/responses` as the OpenAI Responses API does, with and without a stream. A stream ends with `response.completed`, `response.incomplete` or `response.failed`, chosen with `--terminal` or the request header `x-mock-terminal`. The perf lane measures `responses_stream_plain_50` in its default set and `responses_stream_plain_1000` when `PERF_SCENARIOS` names it, the soak test runs `agent_responses_stream`, and `test/t/cases/agent_proxy.rb` checks its recipes on `/v1/responses` as well. No port is added ("Mock LLM upstream" in [docs/test/README.md](/contributing/test/)) (#600)
- New test cases and the ports of their servers: `test/t/cases/gem_dir.rb` (`Dir`, 18127) and `test/t/cases/gem_uname.rb` (mruby-uname, 18128) (#559), `test/t/cases/config_time_log.rb` (the messages logged while nginx reads the configuration, 18140) (#565), `test/t/cases/return_in_block.rb` (a `return` inside a block or a `for` body of handler code, 18117) (#584), `test/t/cases/var_method_lookup.rb` (the accessors of `Nginx::Var` and the methods that read variables, 18159) (#610), `test/t/cases/home_example.rb` (the example of the home page of the site, 18160) (#617), and `test/t/cases/guides.rb` (the samples of the guides and use cases of the site, 18161 to 18168 and the stream port 12364) (#619)
- `dependence/ngx_devel_kit/` holds the tree of the ngx_devel_kit tag `v0.3.4` (commit `bd44d1630`), with the one change of ngx_mruby kept: in `src/ndk_set_var.c`, an `NGX_DONE` that the variable handler of `mruby_set` returns becomes the status of nginx's script engine. The tree was merged with `update-devkit-subtree v0.3.4` as a subtree squash that records `git-subtree-split: bd44d16302273052d6005d7bdb55f74e23813de3`, so the next `update-devkit-subtree` merges from that commit. The tree before was the tag `v0.3.1` (`a22dade76`), whose `src/ndk.h` said 0.2.15. ngx_mruby enables only the `set_var` part of NDK (`have=NDK_SET_VAR` in `config.in`). Of the files that such a build compiles, the commits since v0.3.1 change only `src/ndk.h` and `src/ndk_string_util.h`, in lines that produce no machine code: the version macros of `src/ndk.h` are used nowhere, NDK's `config` always defines `NDK`, so the removed `#error` never applied, and `ndk_memcpyp` is a macro whose only user, `src/ndk_conf_file.c`, is not compiled. The commits fix the argument that `ndk_conf_set_http_complex_path_slot` passes to its post handler and the size that `ndk_memcpyp` copies, correct the type in the size of the bucket array that `ndk_upstream_list` allocates (the number of bytes stays the same), remove the `#error` that `src/ndk.h` raised in a build without `NDK`, and set `NDK_VERSION` to "0.3.4". A build that also adds a module which enables those parts of NDK compiles the fixed code (#597)
- nginx's own test suite: `sh test/nginx-tests/run.sh` runs [nginx-tests](https://github.com/nginx/nginx-tests) at a pinned commit with `prove` on nginx built with ngx_mruby by `test.sh` and on nginx of the same version and configure options without ngx_mruby, and writes the counts of both builds and the test files that fail on each to `build_nginxtests/summary.md`. It needs Linux, a user other than root and perl, and the test files listen on ports from 8000 to 8999. `TEST_SH_SKIP_TESTS=1 sh test.sh` stops after the build and the install. The `nginx-tests` workflow of CI runs it on pull requests to `next` that change the nginx binary, `test.sh` or the runner, and on pushes to `next`; the required check `ci-ok` does not need it ("nginx's own test suite (nginx-tests)" in [docs/test/README.md](/contributing/test/)) (#602)
- The memory soak test also checks `Nginx::Debug` itself: in each scenario of `test/soak/nginx.conf` it requests the new location `/debug/check` once before the warmup, and the scenario fails unless `Nginx::Debug.stats` has the documented keys with Integer values, `Nginx::Debug.gc` returns `nil`, and the full GC of `Nginx::Debug.gc` frees the objects that nothing references ("Soak test for memory" in [docs/test/README.md](/contributing/test/)) (#605)
- The example of the start page of the site: `examples/start/` holds a `compose.yaml` and a `Dockerfile` that build nginx with ngx_mruby from the checkout, with an `nginx.conf` and a Ruby script, and its `README.md` is the page `/start/`. The `examples` workflow of CI runs the commands of that README in order with `examples/check_example.py` and compares their output with what the README states, on pull requests to `next` and pushes to `next` that change `examples/`, the workflow or the inputs of the build (`src/`, `mrbgems/`, `mruby/`, `dependence/`, `build.sh`, `build_config.rb`, `build_config.rb.lock`, `nginx_version`, `configure`, `config.in`, `Makefile.in`); the required check `ci-ok` does not need it (#618)
- `test/t/cases/agent_proxy.rb` reads the values that it compares for the recipes of the Messages API from `test/html/agent_proxy_auth.expected`, `agent_proxy_route.expected`, `agent_proxy_limit.expected` and `agent_proxy_log.expected`, one `key=value` per line, or `key~=text` for a value that contains the text, which the agent proxy page of the site shows; `test/t/cases/_agent_proxy_client.rb` takes the port of nginx as an optional third argument (#622)
- `test.sh` stops only the nginx that it started, by its pid. It no longer runs `killall nginx` (`pkill nginx` on NetBSD, FreeBSD and CentOS) before it starts the test nginx, which stopped every nginx on the machine. Before the start, it stops the nginx that an earlier run in the same build directory left running, by the pid in `logs/nginx.pid`, when that process runs the `sbin/nginx` of this build. When it exits, also after a failure and on SIGHUP, SIGINT or SIGTERM (once the command that runs at that moment, such as the wait for nginx or the current test file, has ended), it sends SIGTERM to the test nginx and sends it again 1, 3, 7, 15 and 31 seconds later while the process still runs, because a single nginx process can miss a SIGTERM that arrives right before it waits for events; after `NGINX_STOPTIME` seconds (default 60) it sends SIGKILL and exits 1. Before each signal, it checks that the pid still runs the `sbin/nginx` of this build, so a pid that another process took after nginx ended gets no signal. `test/t/cases/_second_instance.rb` sends the SIGQUIT to its nginx again in the same way ("Stopping nginx" in [docs/test/README.md](/contributing/test/)) (#635)
- `TEST_SH_PORT_OFFSET=N` (an integer from -2345 to 47336, default 0) moves every port of the test suite by N, so that two checkouts can run `test.sh` at the same time on one machine, each with its own offset. The ports of the suite are the numbers from 12345 to 12399 and from 18080 to 18199 in `test/t`, `test/conf`, `test/html` and the two files of `test/soak` that the agent proxy tests load. `test.sh` copies these files with the new script `test/t/cases/_stage.rb` to `build/nginx/harness/test/`, moving each such number written as five digits by N, and installs the configuration and the hook scripts and runs the tests from that copy; the tests run the CRuby helpers of the copy through the new method `suite_path` of `test/t/cases/_prelude.rb`. With offset 0 the copy is the same as the source. `_stage.rb` fails for such a number written with an underscore and for an offset that would move a port out of 10000 to 65535 ("Ports" in [docs/test/README.md](/contributing/test/)) (#641)
- The message that `test.sh` prints when it sends SIGKILL to the test nginx appears only when it sent the signal: when the pid no longer runs this build's nginx at that moment, the stop ends without the signal and counts as ended, so `test.sh` keeps the exit status of the tests. Before, it printed that it sent SIGKILL, and `test.sh` exited 1; at the start, the stop of the nginx left by an earlier run ended the run (#641)
- valgrind fails the CI cell that runs nginx under it: such a cell runs nginx with `valgrind --error-exitcode=99 --leak-check=full --errors-for-leak-kinds=definite,indirect` and the suppressions of the new file `test/valgrind.supp`, and its step "Check the valgrind reports" fails the cell when the `ERROR SUMMARY` of a valgrind log counts an error: a memory error, or a definitely or indirectly lost block that the file does not suppress. The file suppresses the environment array that nginx leaves allocated at exit (`ngx_set_environment()`, 8 bytes). Possibly lost blocks are printed but are not errors. On pull requests and pushes, one cell of the `build` matrix (nginx 1.31.6, static module) runs nginx under valgrind, and the other cells without a sanitizer run it directly. A manual run of `test.yml` with the new input `valgrind` runs every cell except the sanitizer cell under valgrind; the new workflow `valgrind-nightly.yml` starts such a run every day once the file is on `master`, the default branch, and until then on pull requests that change it, `test.yml` or `test/valgrind.supp`. `test.sh` still does not take valgrind's errors into its exit status ("valgrind in CI" in [docs/test/README.md](/contributing/test/)) (#634)
- The site is deployed: the job `deploy` of `.github/workflows/site.yml` publishes the site that the job `build` built and checked to GitHub Pages at `https://ngx.mruby.org/`, on a push to `next` and on a `workflow_dispatch` on `next`, never for a pull request. It uses `actions/configure-pages`, `actions/upload-pages-artifact` and `actions/deploy-pages`, pinned by commit, with the permissions `pages: write` and `id-token: write` only, and stops unless the Pages settings give the origin `https://ngx.mruby.org`. The site serves the Google verification file `googleba3435e4002729b6.html` of the old site at the same URL and loads no analytics script, which `site/scripts/check-deploy.mjs` checks on every run; `site/scripts/check-live.mjs` checks the live site after a deployment, by hand ("The site" in [AGENTS.md](https://github.com/matsumotory/ngx_mruby/blob/next/AGENTS.md)) (#653)
- The site builds the documentation of 2.x under `/v2/` from `docs/` of `master`: `node site/scripts/build-v2.mjs --tree DIR` (`npm run build:v2` in `site/`), after `npm run build`, writes the 2.x pages into `site/dist/v2/` with a search index of their own, and `site/scripts/check-v2.mjs` checks the 2.x banner on every 2.x page, the version links between `/v2/` and `/docs/`, and that the search of each version shows only its own pages. The `site` workflow checks out `master` and builds `/v2/` on every run. On `master`, the `site-v2` workflow builds the site with the `docs/` of every pull request to `master` and fails on a broken `/v2/` link, and the `site-dispatch` workflow starts the `site` workflow on `next` after every push to `master` (#657, #656)

## Upgrading

Supported nginx versions: the nginx mainline and stable lines (see
[README.md](https://github.com/matsumotory/ngx_mruby/blob/next/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](/releases/migration/) has the
steps for finding the scripts and builds that each entry affects. Check
these before upgrading:

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