unit: Doesn't work after rails 7.1 update

Updated a rails app from 7.0 to 7.1. Getting the following error in unit logs:

2023/10/18 02:02:58 [error] 977151#977151 [unit] #9: Ruby: Wrong header entry 'value' from application
2023/10/18 02:02:58 [error] 977151#977151 [unit] #9: Ruby: Failed to run ruby script

might be related to rack upgrade from 2.x to 3.x. Locking rack to ~> 2.0 fixes the issue.

Unit version: 1.31.0.

About this issue

  • Original URL
  • State: closed
  • Created 8 months ago
  • Comments: 29 (23 by maintainers)

Commits related to this issue

Most upvoted comments

Will apply and give it a spin! Should add some pytest for this as well! But LGTM! @xeron great first issue! Thanks!!

@ac000 BTW I just realized your build of my sample app fails because of the memory usage. Seems like docker doesn’t have enough memory for bundle install command.

Anyway, I applied your patch from https://github.com/nginx/unit/issues/974#issuecomment-1771309585 and rerun the test:

I, [2023-10-22T20:57:28.596104 #56]  INFO -- : [0cb91252-f64f-4051-9250-4b061c2bd283] Started GET "/" for 172.17.0.1 at 2023-10-22 20:57:28 +0000
I, [2023-10-22T20:57:28.596678 #56]  INFO -- : [0cb91252-f64f-4051-9250-4b061c2bd283] Processing by PostsController#index as */*
I, [2023-10-22T20:57:28.601112 #56]  INFO -- : [0cb91252-f64f-4051-9250-4b061c2bd283]   Rendered layout layouts/application.html.erb (Duration: 3.1ms | Allocations: 4556)
I, [2023-10-22T20:57:28.601211 #56]  INFO -- : [0cb91252-f64f-4051-9250-4b061c2bd283] Completed 200 OK in 4ms (Views: 2.9ms | ActiveRecord: 0.6ms | Allocations: 6239)
RDBG: r_key : x-frame-options
RDBG: TYPE(r_value) : STRING
RDBG: r_value : SAMEORIGIN
RDBG: r_key : x-xss-protection
RDBG: TYPE(r_value) : STRING
RDBG: r_value : 0
RDBG: r_key : x-content-type-options
RDBG: TYPE(r_value) : STRING
RDBG: r_value : nosniff
RDBG: r_key : x-permitted-cross-domain-policies
RDBG: TYPE(r_value) : STRING
RDBG: r_value : none
RDBG: r_key : referrer-policy
RDBG: TYPE(r_value) : STRING
RDBG: r_value : strict-origin-when-cross-origin
RDBG: r_key : link
RDBG: TYPE(r_value) : STRING
RDBG: r_value : </assets/application-e0cf9d8fcb18bf7f909d8d91a5e78499f82ac29523d475bf3a9ab265d5e2b451.css>; rel=preload; as=style; nopush,</assets/es-module-shims.min-4ca9b3dd5e434131e3bb4b0c1d7dff3bfd4035672a5086deec6f73979a49be73.js>; rel=preload; as=script; nopush
RDBG: r_key : content-type
RDBG: TYPE(r_value) : STRING
RDBG: r_value : text/html; charset=utf-8
RDBG: r_key : vary
RDBG: TYPE(r_value) : STRING
RDBG: r_value : Accept
RDBG: r_key : etag
RDBG: TYPE(r_value) : STRING
RDBG: r_value : W/"a6e66637d9726009ee1f0f7b5c56aac4"
RDBG: r_key : cache-control
RDBG: TYPE(r_value) : STRING
RDBG: r_value : max-age=0, private, must-revalidate
RDBG: r_key : set-cookie
RDBG: TYPE(r_value) : ARRAY
2023/10/22 20:57:28 [error] 56#56 [unit] #8: Ruby: Wrong header entry 'value' from application
RDBG: r_value : -
2023/10/22 20:57:28 [error] 56#56 [unit] #8: Ruby: Failed to run ruby script

Quick search through rack PRs points to https://github.com/rack/rack/pull/1793 and https://github.com/rack/rack/issues/2129.

Seems like starting with rack 3.0.0 header could be represented by a String or an Array of Strings.

The fix for this has been merged.

This patch should make things work

It handles empty response headers and array response fields.

E.g The following

app = Proc.new do |env|                                                         
    ["200", {                                                                   
        "Content-Type" => "text/plain",                                         
        "X-Empty-Header" => nil,
        "X-Array-Header" => ["Item-1", nil, "Item-3", "Item-4"],
    }, ["Hello World\n"]]                                                         
end                                                                             
                                                                                
run app

produces

$ curl -v http://localhost:8080/
*   Trying 127.0.0.1:8080...
* connect to 127.0.0.1 port 8080 failed: Connection refused
*   Trying [::1]:8080...
* Connected to localhost (::1) port 8080 (#0)
> GET / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/8.0.1
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Type: text/plain
< X-Empty-Header: 
< X-Array-Header: Item-1; ; Item-3; Item-4
< Server: Unit/1.31.1
< Date: Mon, 23 Oct 2023 13:28:46 GMT
< Transfer-Encoding: chunked
< 
Hello World
* Connection #0 to host localhost left intact

You’ll notice it also handles nil array values.

diff --git a/src/ruby/nxt_ruby.c b/src/ruby/nxt_ruby.c
index bcb48f6b..55f9966e 100644
--- a/src/ruby/nxt_ruby.c
+++ b/src/ruby/nxt_ruby.c
@@ -874,6 +874,18 @@ nxt_ruby_rack_result_headers(nxt_unit_request_info_t *req, VALUE result,
 }
 
 
+#define NXT_RUBY_SET_HDR_VALUE(r_value, value, value_end)                     \
+    do {                                                                      \
+        if (TYPE(r_value) == T_STRING) {                                      \
+            value = RSTRING_PTR(r_value);                                     \
+            value_end = value + RSTRING_LEN(r_value);                         \
+        } else {                                                              \
+            value = "";                                                       \
+            value_end = value;                                                \
+        }                                                                     \
+    } while (0);
+
+
 static int
 nxt_ruby_hash_info(VALUE r_key, VALUE r_value, VALUE arg)
 {
@@ -889,16 +901,44 @@ nxt_ruby_hash_info(VALUE r_key, VALUE r_value, VALUE arg)
         goto fail;
     }
 
-    if (nxt_slow_path(TYPE(r_value) != T_STRING)) {
+    if (nxt_slow_path(TYPE(r_value) != T_STRING
+                      && TYPE(r_value) != T_ARRAY
+                      && TYPE(r_value) != T_NIL)) {
         nxt_unit_req_error(headers_info->req,
                            "Ruby: Wrong header entry 'value' from application");
 
         goto fail;
     }
 
-    value = RSTRING_PTR(r_value);
-    value_end = value + RSTRING_LEN(r_value);
+    if (TYPE(r_value) == T_ARRAY) {
+        int     i;
+        int     arr_len = RARRAY_LEN(r_value);
+        VALUE   item;
+        size_t  len = 0;
 
+        for (i = 0; i < arr_len; i++) {
+            item = rb_ary_entry(r_value, i);
+            if (TYPE(item) != T_STRING && TYPE(item) != T_NIL) {
+                nxt_unit_req_error(headers_info->req,
+                                   "Ruby: Wrong header entry in 'value' array "
+                                   "from application");
+                goto fail;
+            }
+
+            if (TYPE(item) == T_STRING) {
+                len += RSTRING_LEN(item);
+            }
+
+            len += 2;    /* +2 for '; ' */
+        }
+
+        headers_info->fields++;
+        headers_info->size += RSTRING_LEN(r_key) + len - 2;
+
+        return ST_CONTINUE;
+    }
+
+    NXT_RUBY_SET_HDR_VALUE(r_value, value, value_end);
     pos = value;
 
     for ( ;; ) {
@@ -941,11 +981,57 @@ nxt_ruby_hash_add(VALUE r_key, VALUE r_value, VALUE arg)
     headers_info = (void *) (uintptr_t) arg;
     rc = &headers_info->rc;
 
-    value = RSTRING_PTR(r_value);
-    value_end = value + RSTRING_LEN(r_value);
-
     key_len = RSTRING_LEN(r_key);
 
+    if (TYPE(r_value) == T_ARRAY) {
+        int     i;
+        int     arr_len = RARRAY_LEN(r_value);
+        char    *field, *p;
+        VALUE   item;
+        size_t  len = 0;
+
+        for (i = 0; i < arr_len; i++) {
+            item = rb_ary_entry(r_value, i);
+
+            if (TYPE(item) == T_STRING) {
+                len += RSTRING_LEN(item);
+            }
+
+            len += 2;    /* +2 for '; ' */
+        }
+
+        field = nxt_malloc(len);
+        if (field == NULL) {
+            goto fail;
+        }
+
+        p = field;
+
+        for (i = 0; i < arr_len; i++) {
+            item = rb_ary_entry(r_value, i);
+            if (TYPE(item) == T_STRING) {
+                p = nxt_cpymem(p, RSTRING_PTR(item), RSTRING_LEN(item));
+            }
+
+            p = nxt_cpymem(p, "; ", 2);
+        }
+
+        len -= 2;
+
+        *rc = nxt_unit_response_add_field(headers_info->req,
+                                          RSTRING_PTR(r_key), key_len,
+                                          field, len);
+        nxt_free(field);
+
+        if (nxt_slow_path(*rc != NXT_UNIT_OK)) {
+            goto fail;
+        }
+
+        return ST_CONTINUE;
+    }
+
+    NXT_RUBY_SET_HDR_VALUE(r_value, value, value_end);
+
     pos = value;
 
     for ( ;; ) {

I seem to have managed to setup a Rails 7.1.1 environment on Fedora 38 and created the basic rails app from here by doing

$ rails new gh-974

My Unit config is simply

{
    "listeners": {
        "[::1]:8080": {
            "pass": "applications/app"
        }
    },

     "applications": {
        "app": {
            "type": "ruby",
            "working_directory": "/home/andrew/src/ruby/gh-974",
            "script": "config.ru"
        }
    }
}

Hitting the app gives…

<!DOCTYPE html>
<html>
<head>
  <title>Ruby on Rails 7.1.1</title>
...
  <ul>
    <li><strong>Rails version:</strong> 7.1.1</li>
    <li><strong>Ruby version:</strong> ruby 3.2.2 (2023-03-30 revision e51014f9c0) [x86_64-linux]</li>
  </ul>
</body>

</html>

This is using Rack 3.0.8 as confirmed by strace(1)

15685 openat(AT_FDCWD, "/home/andrew/.local/share/gem/ruby/gems/rack-3.0.8/lib/rack/head.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = 18
15685 openat(AT_FDCWD, "/home/andrew/.local/share/gem/ruby/gems/rack-3.0.8/lib/rack/head.rb", O_RDONLY) = 18
15685 openat(AT_FDCWD, "/home/andrew/.local/share/gem/ruby/gems/rack-3.0.8/lib/rack/conditional_get.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = 18
15685 openat(AT_FDCWD, "/home/andrew/.local/share/gem/ruby/gems/rack-3.0.8/lib/rack/conditional_get.rb", O_RDONLY) = 18
15685 openat(AT_FDCWD, "/home/andrew/.local/share/gem/ruby/gems/rack-3.0.8/lib/rack/etag.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = 18
15685 openat(AT_FDCWD, "/home/andrew/.local/share/gem/ruby/gems/rack-3.0.8/lib/rack/etag.rb", O_RDONLY) = 18
15685 openat(AT_FDCWD, "/home/andrew/.local/share/gem/ruby/gems/rack-3.0.8/lib/rack/tempfile_reaper.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = 18
15685 openat(AT_FDCWD, "/home/andrew/.local/share/gem/ruby/gems/rack-3.0.8/lib/rack/tempfile_reaper.rb", O_RDONLY) = 18
15685 openat(AT_FDCWD, "/home/andrew/.local/share/gem/ruby/gems/rack-3.0.8/lib/rack/files.rb", O_RDONLY|O_NONBLOCK|O_CLOEXEC) = 18
15685 openat(AT_FDCWD, "/home/andrew/.local/share/gem/ruby/gems/rack-3.0.8/lib/rack/files.rb", O_RDONLY) = 18