]> git.kaiwu.me - njs.git/commitdiff
Modules: bind QuickJS native views to their context lifetime
authorDmitry Volyntsev <xeioex@nginx.com>
Tue, 10 Nov 2026 20:17:46 +0000 (12:17 -0800)
committerDmitry Volyntsev <xeioexception@gmail.com>
Wed, 11 Nov 2026 21:33:45 +0000 (13:33 -0800)
HTTP and stream views can outlive their request or connection pool
in a reused runtime.  Detaching only the root object leaves variables,
headers, forms and replies with dangling native pointers.  Direct
session accessors and cold body getters can dereference detached roots,
while populated request caches hide their expiry.

Share a runtime-owned revocable token across each binding and its
views.  Reject expired access before native dereferences, including
cache access, without retaining pools or copying native data.
Keep reply tokens tied to the originating subrequest operation so
completed replies remain usable until the parent binding is detached.

The header and variable lifetime gap dates back to 201b1276, which
introduces QuickJS integration.  RequestForm uses the same unguarded
model since 0ffc96df.  Extracted JS values and stream callback flags
remain independent; RequestForm remains a guarded request-bound view.

nginx/ngx_http_js_module.c
nginx/ngx_js.c
nginx/ngx_js.h
nginx/ngx_stream_js_module.c
nginx/t/js_http_lifetime_scopes.t [new file with mode: 0644]
nginx/t/js_http_object_lifetime.t [new file with mode: 0644]
nginx/t/stream_js_object_lifetime.t [new file with mode: 0644]

index 93a9422527ff279edc1dbab29ea87d662882397c..8a9c00338d083875956fdfc2839284d3bee367ba 100644 (file)
@@ -165,6 +165,18 @@ typedef struct {
     JSValue          response_body;
 } ngx_http_qjs_request_t;
 
+
+typedef struct {
+    ngx_js_opaque_t       opaque;
+    ngx_uint_t            buffer_type;
+} ngx_http_qjs_variables_t;
+
+
+typedef struct {
+    ngx_qjs_owner_t      *owner;
+    ngx_http_js_ctx_t    *ctx;
+} ngx_http_qjs_subrequest_t;
+
 #endif
 
 
@@ -236,13 +248,13 @@ static JSValue ngx_http_qjs_ext_read_request_body(JSContext *cx,
 static ngx_int_t ngx_http_qjs_body_resolve(ngx_http_request_t *r,
     ngx_http_js_ctx_t *ctx, void *event);
 static JSValue ngx_http_qjs_form_to_value(JSContext *cx, ngx_http_request_t *r,
-    ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys);
+    ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys, JSValueConst request);
 static JSValue ngx_http_qjs_request_form_entry_value(JSContext *cx,
     ngx_js_form_entry_t *entry);
 static ngx_int_t ngx_http_qjs_request_form_max_keys(JSContext *cx,
     JSValueConst options, ngx_uint_t *max_keys);
 static JSValue ngx_http_qjs_request_form_make(JSContext *cx,
-    ngx_js_form_t *form);
+    JSValueConst request, ngx_js_form_t *form);
 static JSValue ngx_http_qjs_ext_read_request_form(JSContext *cx,
     JSValueConst this_val, int argc, JSValueConst *argv);
 static JSValue ngx_http_qjs_ext_request_form_get(JSContext *cx,
@@ -491,9 +503,14 @@ static int ngx_http_qjs_headers_out_define_own_property(JSContext *cx,
 static int ngx_http_qjs_headers_out_delete_property(JSContext *cx,
     JSValueConst obj, JSAtom prop);
 
-static ngx_http_request_t *ngx_http_qjs_request(JSValueConst val);
+static ngx_http_request_t *ngx_http_qjs_request(JSContext *cx,
+    JSValueConst val);
 static JSValue ngx_http_qjs_request_make(JSContext *cx, ngx_int_t proto_id,
-    ngx_http_request_t *r);
+    ngx_http_request_t *r, ngx_qjs_owner_t *owner);
+static JSValue ngx_http_qjs_view_make(JSContext *cx, JSValueConst request,
+    JSClassID request_class, JSClassID view_class, void *external);
+static JSValue ngx_http_qjs_variables_make(JSContext *cx, JSValueConst request,
+    JSClassID request_class, int type);
 static void ngx_http_qjs_request_mark(JSRuntime *rt, JSValueConst val,
     JS_MarkFunc *mark_func);
 static void ngx_http_qjs_request_finalizer(JSRuntime *rt, JSValue val);
@@ -1483,13 +1500,13 @@ static JSClassDef ngx_http_qjs_periodic_class = {
 
 static JSClassDef ngx_http_qjs_request_form_class = {
     "RequestForm",
-    .finalizer = NULL,
+    .finalizer = ngx_qjs_opaque_finalizer,
 };
 
 
 static JSClassDef ngx_http_qjs_variables_class = {
     "Variables",
-    .finalizer = NULL,
+    .finalizer = ngx_qjs_opaque_finalizer,
     .exotic = & (JSClassExoticMethods) {
         .get_own_property = ngx_http_qjs_variables_own_property,
         .set_property = ngx_http_qjs_variables_set_property,
@@ -1499,7 +1516,7 @@ static JSClassDef ngx_http_qjs_variables_class = {
 
 static JSClassDef ngx_http_qjs_headers_in_class = {
     "headersIn",
-    .finalizer = NULL,
+    .finalizer = ngx_qjs_opaque_finalizer,
     .exotic = & (JSClassExoticMethods) {
         .get_own_property = ngx_http_qjs_headers_in_own_property,
         .get_own_property_names = ngx_http_qjs_headers_in_own_property_names,
@@ -1509,7 +1526,7 @@ static JSClassDef ngx_http_qjs_headers_in_class = {
 
 static JSClassDef ngx_http_qjs_headers_out_class = {
     "headersOut",
-    .finalizer = NULL,
+    .finalizer = ngx_qjs_opaque_finalizer,
     .exotic = & (JSClassExoticMethods) {
         .get_own_property = ngx_http_qjs_headers_out_own_property,
         .get_own_property_names = ngx_http_qjs_headers_out_own_property_names,
@@ -6157,9 +6174,10 @@ ngx_http_qjs_ext_args(JSContext *cx, JSValueConst this_val)
     ngx_http_request_t      *r;
     ngx_http_qjs_request_t  *req;
 
-    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_get(cx, this_val,
+                                               NGX_QJS_CLASS_ID_HTTP_REQUEST);
     if (req == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     if (!JS_IsUndefined(req->args)) {
@@ -6313,9 +6331,9 @@ ngx_http_qjs_ext_done(JSContext *cx, JSValueConst this_val,
     ngx_http_js_ctx_t   *ctx;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
@@ -6337,9 +6355,9 @@ ngx_http_qjs_ext_finish(JSContext *cx, JSValueConst this_val,
     ngx_http_js_ctx_t   *ctx;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     if (ngx_http_send_special(r, NGX_HTTP_LAST) == NGX_ERROR) {
@@ -6357,39 +6375,18 @@ ngx_http_qjs_ext_finish(JSContext *cx, JSValueConst this_val,
 static JSValue
 ngx_http_qjs_ext_headers_in(JSContext *cx, JSValueConst this_val)
 {
-    JSValue              obj;
-    ngx_http_request_t  *r;
-
-    r = ngx_http_qjs_request(this_val);
-    if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
-    }
-
-    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN);
-
-    JS_SetOpaque(obj, r);
-
-    return obj;
+    return ngx_http_qjs_view_make(cx, this_val,
+                                 NGX_QJS_CLASS_ID_HTTP_REQUEST,
+                                 NGX_QJS_CLASS_ID_HTTP_HEADERS_IN, NULL);
 }
 
 
 static JSValue
 ngx_http_qjs_ext_headers_out(JSContext *cx, JSValueConst this_val)
 {
-    JSValue              obj;
-    ngx_http_request_t  *r;
-
-    r = ngx_http_qjs_request(this_val);
-    if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
-    }
-
-    obj = JS_NewObjectProtoClass(cx, JS_NULL,
-                                 NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
-
-    JS_SetOpaque(obj, r);
-
-    return obj;
+    return ngx_http_qjs_view_make(cx, this_val,
+                                 NGX_QJS_CLASS_ID_HTTP_REQUEST,
+                                 NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT, NULL);
 }
 
 
@@ -6399,9 +6396,9 @@ ngx_http_qjs_ext_http_version(JSContext *cx, JSValueConst this_val)
     ngx_str_t            v;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     switch (r->http_version) {
@@ -6441,9 +6438,9 @@ ngx_http_qjs_ext_internal(JSContext *cx, JSValueConst this_val)
 {
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     return JS_NewBool(cx, r->internal);
@@ -6457,9 +6454,9 @@ ngx_http_qjs_ext_internal_redirect(JSContext *cx, JSValueConst this_val,
     ngx_http_js_ctx_t   *ctx;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     if (r->parent != NULL) {
@@ -6492,9 +6489,9 @@ ngx_http_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc,
     const char          *msg;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     for (n = 0; n < argc; n++) {
@@ -6516,24 +6513,8 @@ static JSValue
 ngx_http_qjs_ext_periodic_variables(JSContext *cx,
     JSValueConst this_val, int type)
 {
-    JSValue                  obj;
-    ngx_http_qjs_request_t  *req;
-
-    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_PERIODIC);
-    if (req == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a periodic object");
-    }
-
-    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_VARS);
-
-    /*
-     * Using lowest bit of the pointer to store the buffer type.
-     */
-    type = (type == NGX_JS_BUFFER) ? 1 : 0;
-    JS_SetOpaque(obj, (void *) ((uintptr_t) req->opaque.external
-                              | (uintptr_t) type));
-
-    return obj;
+    return ngx_http_qjs_variables_make(cx, this_val,
+                                      NGX_QJS_CLASS_ID_HTTP_PERIODIC, type);
 }
 
 
@@ -6543,9 +6524,9 @@ ngx_http_qjs_ext_parent(JSContext *cx, JSValueConst this_val)
     ngx_http_js_ctx_t   *ctx;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     ctx = r->parent ? ngx_http_get_module_ctx(r->parent, ngx_http_js_module)
@@ -6565,9 +6546,9 @@ ngx_http_qjs_ext_remote_address(JSContext *cx, JSValueConst this_val)
     ngx_connection_t    *c;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     c = r->connection;
@@ -6587,9 +6568,10 @@ ngx_http_qjs_ext_response_body(JSContext *cx, JSValueConst this_val, int type)
     ngx_http_request_t      *r;
     ngx_http_qjs_request_t  *req;
 
-    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_get(cx, this_val,
+                                               NGX_QJS_CLASS_ID_HTTP_REQUEST);
     if (req == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     buffer_type = ngx_js_buffer_type(type);
@@ -6642,9 +6624,10 @@ ngx_http_qjs_ext_request_body(JSContext *cx, JSValueConst this_val, int type)
     ngx_http_js_ctx_t       *ctx;
     ngx_http_qjs_request_t  *req;
 
-    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_get(cx, this_val,
+                                               NGX_QJS_CLASS_ID_HTTP_REQUEST);
     if (req == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     buffer_type = ngx_js_buffer_type(type);
@@ -6739,7 +6722,8 @@ ngx_http_qjs_body_resolve(ngx_http_request_t *r, ngx_http_js_ctx_t *ctx,
     cx = ctx->engine->u.qjs.ctx;
 
     if (ngx_http_js_body_read_is_form(ctx->body_read_state)) {
-        result = ngx_http_qjs_form_to_value(cx, r, ctx, (uintptr_t) ev->data);
+        result = ngx_http_qjs_form_to_value(cx, r, ctx, (uintptr_t) ev->data,
+                                           ev->args[2]);
 
     } else {
         result = ngx_http_qjs_body_to_value(cx, ctx, (uintptr_t) ev->data);
@@ -6774,9 +6758,10 @@ ngx_http_qjs_ext_read_request_body(JSContext *cx, JSValueConst this_val,
     ngx_http_request_t      *r;
     ngx_http_qjs_request_t  *req;
 
-    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_get(cx, this_val,
+                                               NGX_QJS_CLASS_ID_HTTP_REQUEST);
     if (req == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     r = req->opaque.external;
@@ -6790,7 +6775,7 @@ ngx_http_qjs_ext_read_request_body(JSContext *cx, JSValueConst this_val,
         return JS_ThrowTypeError(cx, "request body is already being read");
     }
 
-    event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 2);
+    event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 3);
     if (event == NULL) {
         return JS_ThrowOutOfMemory(cx);
     }
@@ -6806,6 +6791,7 @@ ngx_http_qjs_ext_read_request_body(JSContext *cx, JSValueConst this_val,
     }
 
     event->function = JS_DupValue(cx, event->args[0]);
+    event->args[2] = JS_DupValue(cx, this_val);
     event->destructor = ngx_http_js_read_body_event_destructor;
 
     ngx_js_add_event(ctx, event);
@@ -6872,24 +6858,18 @@ ngx_http_qjs_request_form_max_keys(JSContext *cx, JSValueConst options,
 
 
 static JSValue
-ngx_http_qjs_request_form_make(JSContext *cx, ngx_js_form_t *form)
+ngx_http_qjs_request_form_make(JSContext *cx, JSValueConst request,
+    ngx_js_form_t *form)
 {
-    JSValue  obj;
-
-    obj = JS_NewObjectClass(cx, NGX_QJS_CLASS_ID_HTTP_FORM);
-    if (JS_IsException(obj)) {
-        return JS_EXCEPTION;
-    }
-
-    JS_SetOpaque(obj, form);
-
-    return obj;
+    return ngx_http_qjs_view_make(cx, request,
+                                 NGX_QJS_CLASS_ID_HTTP_REQUEST,
+                                 NGX_QJS_CLASS_ID_HTTP_FORM, form);
 }
 
 
 static JSValue
 ngx_http_qjs_form_to_value(JSContext *cx, ngx_http_request_t *r,
-    ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys)
+    ngx_http_js_ctx_t *ctx, ngx_uint_t max_keys, JSValueConst request)
 {
     ngx_int_t       rc;
     ngx_str_t       error;
@@ -6897,7 +6877,7 @@ ngx_http_qjs_form_to_value(JSContext *cx, ngx_http_request_t *r,
 
     rc = ngx_http_js_request_form(r, ctx, max_keys, &form, &error);
     if (rc == NGX_OK) {
-        return ngx_http_qjs_request_form_make(cx, form);
+        return ngx_http_qjs_request_form_make(cx, request, form);
     }
 
     if (rc == NGX_JS_FORM_TYPE_ERROR) {
@@ -6923,15 +6903,22 @@ ngx_http_qjs_ext_read_request_form(JSContext *cx, JSValueConst this_val,
     ngx_http_request_t      *r;
     ngx_http_qjs_request_t  *req;
 
-    req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_get(cx, this_val,
+                                               NGX_QJS_CLASS_ID_HTTP_REQUEST);
     if (req == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     if (ngx_http_qjs_request_form_max_keys(cx, argv[0], &max_keys) != NGX_OK) {
         return JS_EXCEPTION;
     }
 
+    if (ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST)
+        == NULL)
+    {
+        return JS_EXCEPTION;
+    }
+
     r = req->opaque.external;
     ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
 
@@ -6943,7 +6930,7 @@ ngx_http_qjs_ext_read_request_form(JSContext *cx, JSValueConst this_val,
         return JS_ThrowTypeError(cx, "request body is already being read");
     }
 
-    event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 2);
+    event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t) + sizeof(JSValue) * 3);
     if (event == NULL) {
         return JS_ThrowOutOfMemory(cx);
     }
@@ -6959,6 +6946,7 @@ ngx_http_qjs_ext_read_request_form(JSContext *cx, JSValueConst this_val,
     }
 
     event->function = JS_DupValue(cx, event->args[0]);
+    event->args[2] = JS_DupValue(cx, this_val);
     event->destructor = ngx_http_js_read_body_event_destructor;
 
     ngx_js_add_event(ctx, event);
@@ -6975,7 +6963,7 @@ resolve:
         return JS_ThrowInternalError(cx, "failed to read request body");
     }
 
-    return ngx_http_qjs_form_to_value(cx, r, ctx, max_keys);
+    return ngx_http_qjs_form_to_value(cx, r, ctx, max_keys, this_val);
 }
 
 
@@ -6988,18 +6976,25 @@ ngx_http_qjs_ext_request_form_get(JSContext *cx, JSValueConst this_val,
     const char           *name;
     ngx_uint_t            i, n;
     ngx_js_form_t        *form;
+    ngx_js_opaque_t      *opaque;
     ngx_js_form_entry_t  *entry;
 
-    form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM);
-    if (form == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a RequestForm");
+    opaque = ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM);
+    if (opaque == NULL) {
+        return JS_EXCEPTION;
     }
 
+    form = opaque->external;
     name = JS_ToCStringLen(cx, &name_len, argv[0]);
     if (name == NULL) {
         return JS_ThrowTypeError(cx, "\"name\" must be a string");
     }
 
+    if (ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM) == NULL) {
+        JS_FreeCString(cx, name);
+        return JS_EXCEPTION;
+    }
+
     if (as_array) {
         array = JS_NewArray(cx);
         if (JS_IsException(array)) {
@@ -7060,18 +7055,24 @@ ngx_http_qjs_ext_request_form_has(JSContext *cx, JSValueConst this_val,
     const char           *name;
     ngx_uint_t            i;
     ngx_js_form_t        *form;
+    ngx_js_opaque_t      *opaque;
     ngx_js_form_entry_t  *entry;
 
-    form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM);
-    if (form == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a RequestForm");
+    opaque = ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM);
+    if (opaque == NULL) {
+        return JS_EXCEPTION;
     }
 
+    form = opaque->external;
     name = JS_ToCStringLen(cx, &name_len, argv[0]);
     if (name == NULL) {
         return JS_ThrowTypeError(cx, "\"name\" must be a string");
     }
 
+    if (ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM) == NULL) {
+        JS_FreeCString(cx, name);
+        return JS_EXCEPTION;
+    }
     entry = form->entries.elts;
 
     for (i = 0; i < form->entries.nelts; i++) {
@@ -7096,13 +7097,15 @@ ngx_http_qjs_ext_request_form_for_each(JSContext *cx, JSValueConst this_val,
     JSValue               args[3], ret;
     ngx_uint_t            i;
     ngx_js_form_t        *form;
+    ngx_js_opaque_t      *opaque;
     ngx_js_form_entry_t  *entry;
 
-    form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM);
-    if (form == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a RequestForm");
+    opaque = ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM);
+    if (opaque == NULL) {
+        return JS_EXCEPTION;
     }
 
+    form = opaque->external;
     if (!JS_IsFunction(cx, argv[0])) {
         return JS_ThrowTypeError(cx, "\"callback\" is not a function");
     }
@@ -7134,6 +7137,12 @@ ngx_http_qjs_ext_request_form_for_each(JSContext *cx, JSValueConst this_val,
         }
 
         JS_FreeValue(cx, ret);
+
+        if (ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM)
+            == NULL)
+        {
+            return JS_EXCEPTION;
+        }
     }
 
     return JS_UNDEFINED;
@@ -7174,13 +7183,15 @@ static JSValue
 ngx_http_qjs_ext_request_form_has_files(JSContext *cx, JSValueConst this_val,
     int argc, JSValueConst *argv)
 {
-    ngx_js_form_t  *form;
+    ngx_js_form_t    *form;
+    ngx_js_opaque_t  *opaque;
 
-    form = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_FORM);
-    if (form == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a RequestForm");
+    opaque = ngx_qjs_opaque_get(cx, this_val, NGX_QJS_CLASS_ID_HTTP_FORM);
+    if (opaque == NULL) {
+        return JS_EXCEPTION;
     }
 
+    form = opaque->external;
     return JS_NewBool(cx, form->has_files);
 }
 
@@ -7195,6 +7206,7 @@ ngx_http_js_read_body_event_destructor(ngx_qjs_event_t *event)
     JS_FreeValue(cx, event->function);
     JS_FreeValue(cx, event->args[0]);
     JS_FreeValue(cx, event->args[1]);
+    JS_FreeValue(cx, event->args[2]);
 }
 
 
@@ -7208,9 +7220,9 @@ ngx_http_qjs_ext_return(JSContext *cx, JSValueConst this_val,
     ngx_http_request_t        *r;
     ngx_http_complex_value_t   cv;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     if (ngx_qjs_integer(cx, argv[0], &status) != NGX_OK) {
@@ -7261,9 +7273,9 @@ ngx_http_qjs_ext_decline(JSContext *cx, JSValueConst this_val,
     ngx_http_js_ctx_t   *ctx;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
@@ -7279,9 +7291,9 @@ ngx_http_qjs_ext_status_get(JSContext *cx, JSValueConst this_val)
 {
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     return JS_NewInt32(cx, r->headers_out.status);
@@ -7295,9 +7307,9 @@ ngx_http_qjs_ext_status_set(JSContext *cx, JSValueConst this_val,
     ngx_int_t            n;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     if (ngx_qjs_integer(cx, value, &n) != NGX_OK) {
@@ -7317,9 +7329,9 @@ ngx_http_qjs_ext_string(JSContext *cx, JSValueConst this_val, int offset)
     ngx_str_t           *field;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     field = (ngx_str_t *) ((u_char *) r + offset);
@@ -7339,9 +7351,9 @@ ngx_http_qjs_ext_send(JSContext *cx, JSValueConst this_val,
     ngx_http_js_ctx_t   *ctx;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
@@ -7408,9 +7420,9 @@ ngx_http_qjs_ext_send_buffer(JSContext *cx, JSValueConst this_val,
     ngx_http_js_ctx_t   *ctx;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
@@ -7551,9 +7563,9 @@ ngx_http_qjs_ext_send_header(JSContext *cx, JSValueConst this_val,
 {
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     if (ngx_http_set_content_type(r) != NGX_OK) {
@@ -7577,9 +7589,9 @@ ngx_http_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val,
     ngx_js_ctx_t        *ctx;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
@@ -7596,9 +7608,15 @@ ngx_http_qjs_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc)
 {
     ngx_qjs_event_t  *event = data;
 
-    JSValue              reply;
-    JSContext           *cx;
-    ngx_http_js_ctx_t   *ctx, *sctx;
+    JSValue                     reply;
+    JSContext                  *cx;
+    ngx_http_js_ctx_t          *ctx, *sctx;
+    ngx_http_qjs_subrequest_t  *subrequest;
+
+    subrequest = event->data;
+    if (subrequest->owner == NULL || !subrequest->owner->alive) {
+        return NGX_OK;
+    }
 
     if (rc != NGX_OK || r->connection->error || r->buffered) {
         return rc;
@@ -7623,22 +7641,25 @@ ngx_http_qjs_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc)
 
     sctx->done = 1;
 
-    ctx = ngx_http_get_module_ctx(r->parent, ngx_http_js_module);
+    ctx = subrequest->ctx;
 
     ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
                    "js subrequest done s: %ui parent ctx: %p",
                    r->headers_out.status, ctx);
 
-    if (ctx == NULL) {
+    if (ctx == NULL
+        || ngx_http_get_module_ctx(r->parent, ngx_http_js_module) != ctx)
+    {
         ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                       "js subrequest: failed to get the parent context");
 
         return NGX_ERROR;
     }
 
-    cx = ctx->engine->u.qjs.ctx;
+    cx = event->ctx;
 
-    reply = ngx_http_qjs_request_make(cx, NGX_QJS_CLASS_ID_HTTP_REQUEST, r);
+    reply = ngx_http_qjs_request_make(cx, NGX_QJS_CLASS_ID_HTTP_REQUEST, r,
+                                      subrequest->owner);
     if (JS_IsException(reply)) {
         ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
                       "js subrequest reply creation failed");
@@ -7668,13 +7689,17 @@ ngx_http_qjs_subrequest_done(ngx_http_request_t *r, void *data, ngx_int_t rc)
 static void
 ngx_http_js_subrequest_event_destructor(ngx_qjs_event_t *event)
 {
-    JSContext  *cx;
+    JSContext                 *cx;
+    ngx_http_qjs_subrequest_t  *subrequest;
 
     cx = event->ctx;
+    subrequest = event->data;
 
     JS_FreeValue(cx, event->function);
     JS_FreeValue(cx, event->args[0]);
     JS_FreeValue(cx, event->args[1]);
+    ngx_qjs_owner_release(JS_GetRuntime(cx), subrequest->owner);
+    subrequest->owner = NULL;
 }
 
 
@@ -7690,15 +7715,18 @@ ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val,
     ngx_qjs_event_t             *event;
     ngx_http_js_ctx_t           *ctx;
     ngx_http_request_t          *r, *sr;
+    ngx_http_qjs_request_t      *req;
     ngx_http_request_body_t     *rb;
+    ngx_http_qjs_subrequest_t   *subrequest;
     ngx_http_post_subrequest_t  *ps;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
-    ctx = ngx_http_get_module_ctx(r, ngx_http_js_module);
+    ctx = (ngx_http_js_ctx_t *) ngx_qjs_external_ctx(cx,
+                                                   JS_GetContextOpaque(cx));
 
     if (r->subrequest_in_memory) {
         return JS_ThrowTypeError(cx, "subrequest can only be created for "
@@ -7847,7 +7875,8 @@ ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val,
         promise = !!JS_IsUndefined(callback);
 
         event = ngx_pcalloc(r->pool, sizeof(ngx_qjs_event_t)
-                                     + sizeof(JSValue) * 2);
+                                     + sizeof(JSValue) * 2
+                                     + sizeof(ngx_http_qjs_subrequest_t));
         if (event == NULL) {
             return JS_ThrowOutOfMemory(cx);
         }
@@ -7856,6 +7885,8 @@ ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val,
         event->fd = ctx->event_id++;
         event->args = (JSValue *) &event[1];
         event->destructor = ngx_http_js_subrequest_event_destructor;
+        subrequest = (ngx_http_qjs_subrequest_t *) &event->args[2];
+        event->data = subrequest;
 
         if (promise) {
             retval = JS_NewPromiseCapability(cx, &event->args[0]);
@@ -7871,6 +7902,10 @@ ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val,
         }
 
         event->function = JS_DupValue(cx, callback);
+        req = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+        subrequest->owner = req->opaque.owner;
+        subrequest->owner->references++;
+        subrequest->ctx = ctx;
 
         ps->handler = ngx_http_qjs_subrequest_done;
         ps->data = event;
@@ -7885,6 +7920,11 @@ ngx_http_qjs_ext_subrequest(JSContext *cx, JSValueConst this_val,
     if (ngx_http_subrequest(r, &uri, args.len ? &args : NULL, &sr, ps, flags)
         != NGX_OK)
     {
+        if (event != NULL) {
+            ngx_http_js_subrequest_event_destructor(event);
+        }
+
+        JS_FreeValue(cx, retval);
         return JS_ThrowInternalError(cx, "subrequest creation failed");
     }
 
@@ -7953,9 +7993,9 @@ ngx_http_qjs_ext_raw_headers(JSContext *cx, JSValueConst this_val, int out)
     ngx_table_elt_t     *header, *h;
     ngx_http_request_t  *r;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     headers = (out) ? &r->headers_out.headers : &r->headers_in.headers;
@@ -8030,23 +8070,8 @@ ngx_http_qjs_ext_raw_headers(JSContext *cx, JSValueConst this_val, int out)
 static JSValue
 ngx_http_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type)
 {
-    JSValue              obj;
-    ngx_http_request_t  *r;
-
-    r = ngx_http_qjs_request(this_val);
-    if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
-    }
-
-    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_VARS);
-
-    /*
-     * Using lowest bit of the pointer to store the buffer type.
-     */
-    type = (type == NGX_JS_BUFFER) ? 1 : 0;
-    JS_SetOpaque(obj, (void *) ((uintptr_t) r | (uintptr_t) type));
-
-    return obj;
+    return ngx_http_qjs_variables_make(cx, this_val,
+                                      NGX_QJS_CLASS_ID_HTTP_REQUEST, type);
 }
 
 
@@ -8063,9 +8088,9 @@ ngx_http_qjs_ext_js_var_names(JSContext *cx, JSValueConst this_val, int argc,
     ngx_http_variable_t        *v;
     ngx_http_core_main_conf_t  *cmcf;
 
-    r = ngx_http_qjs_request(this_val);
+    r = ngx_http_qjs_request(cx, this_val);
     if (r == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a request object");
+        return JS_EXCEPTION;
     }
 
     prefix = NULL;
@@ -8133,19 +8158,19 @@ ngx_http_qjs_variables_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
     ngx_str_t                   name, name_lc;
     ngx_uint_t                  i, key, start, length, is_capture;
     ngx_http_request_t         *r;
+    ngx_http_qjs_variables_t   *variables;
     ngx_http_variable_value_t  *vv;
     u_char                      storage[64];
 
-    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_VARS);
-
-    buffer_type = ((uintptr_t) r & 1) ? NGX_JS_BUFFER : NGX_JS_STRING;
-    r = (ngx_http_request_t *) ((uintptr_t) r & ~(uintptr_t) 1);
-
-    if (r == NULL) {
-        (void) JS_ThrowTypeError(cx, "\"this\" is not a request object");
+    variables = (ngx_http_qjs_variables_t *) ngx_qjs_opaque_get(cx, obj,
+                                                   NGX_QJS_CLASS_ID_HTTP_VARS);
+    if (variables == NULL) {
         return -1;
     }
 
+    buffer_type = variables->buffer_type;
+    r = variables->opaque.external;
+
     name.data = (u_char *) JS_AtomToCString(cx, prop);
     if (name.data == NULL) {
         return -1;
@@ -8203,6 +8228,10 @@ ngx_http_qjs_variables_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
 
     vv = ngx_http_get_variable(r, &name_lc, key);
     JS_FreeCString(cx, (char *) name.data);
+    if (ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_VARS) == NULL) {
+        return -1;
+    }
+
     if (vv == NULL || vv->not_found) {
         return 0;
     }
@@ -8227,19 +8256,19 @@ ngx_http_qjs_variables_set_property(JSContext *cx, JSValueConst obj,
     ngx_uint_t                  key;
     ngx_http_request_t         *r;
     ngx_http_variable_t        *v;
+    ngx_http_qjs_variables_t   *variables;
     ngx_http_variable_value_t  *vv;
     ngx_http_core_main_conf_t  *cmcf;
     u_char                      storage[64];
 
-    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_VARS);
-
-    r = (ngx_http_request_t *) ((uintptr_t) r & ~(uintptr_t) 1);
-
-    if (r == NULL) {
-        (void) JS_ThrowTypeError(cx, "\"this\" is not a request object");
+    variables = (ngx_http_qjs_variables_t *) ngx_qjs_opaque_get(cx, obj,
+                                                   NGX_QJS_CLASS_ID_HTTP_VARS);
+    if (variables == NULL) {
         return -1;
     }
 
+    r = variables->opaque.external;
+
     name.data = (u_char *) JS_AtomToCString(cx, prop);
     if (name.data == NULL) {
         return -1;
@@ -8275,6 +8304,10 @@ ngx_http_qjs_variables_set_property(JSContext *cx, JSValueConst obj,
         return -1;
     }
 
+    if (ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_VARS) == NULL) {
+        return -1;
+    }
+
     if (v->set_handler != NULL) {
         vv = ngx_pcalloc(r->pool, sizeof(ngx_http_variable_value_t));
         if (vv == NULL) {
@@ -8367,14 +8400,16 @@ ngx_http_qjs_headers_in_own_property_names(JSContext *cx,
 {
     int                  ret;
     JSValue              keys;
+    ngx_js_opaque_t     *opaque;
     ngx_http_request_t  *r;
 
-    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN);
-    if (r == NULL) {
-        (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_in object");
+    opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN);
+    if (opaque == NULL) {
         return -1;
     }
 
+    r = opaque->external;
+
     keys = JS_NewObject(cx);
     if (JS_IsException(keys)) {
         return -1;
@@ -8522,6 +8557,7 @@ ngx_http_qjs_headers_in_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
     int                    ret;
     unsigned               flags;
     ngx_str_t              name;
+    ngx_js_opaque_t       *opaque;
     ngx_http_request_t    *r;
     ngx_http_js_header_t  *h;
 
@@ -8539,12 +8575,13 @@ ngx_http_qjs_headers_in_own_property(JSContext *cx, JSPropertyDescriptor *pdesc,
 #undef header
     };
 
-    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN);
-    if (r == NULL) {
-        (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_in object");
+    opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_IN);
+    if (opaque == NULL) {
         return -1;
     }
 
+    r = opaque->external;
+
     name.data = (u_char *) JS_AtomToCString(cx, prop);
     if (name.data == NULL) {
         return -1;
@@ -8577,14 +8614,16 @@ ngx_http_qjs_headers_out_own_property_names(JSContext *cx,
     int                  ret;
     JSAtom               key;
     JSValue              keys;
+    ngx_js_opaque_t     *opaque;
     ngx_http_request_t  *r;
 
-    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
-    if (r == NULL) {
-        (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_out object");
+    opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
+    if (opaque == NULL) {
         return -1;
     }
 
+    r = opaque->external;
+
     keys = JS_NewObject(cx);
     if (JS_IsException(keys)) {
         return -1;
@@ -9182,14 +9221,16 @@ ngx_http_qjs_headers_out_own_property(JSContext *cx,
 {
     int                   ret;
     ngx_str_t             name;
+    ngx_js_opaque_t      *opaque;
     ngx_http_request_t   *r;
 
-    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
-    if (r == NULL) {
-        (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_out object");
+    opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
+    if (opaque == NULL) {
         return -1;
     }
 
+    r = opaque->external;
+
     name.data = (u_char *) JS_AtomToCString(cx, prop);
     if (name.data == NULL) {
         return -1;
@@ -9221,14 +9262,16 @@ ngx_http_qjs_headers_out_define_own_property(JSContext *cx,
 {
     int                   ret;
     ngx_str_t             name;
+    ngx_js_opaque_t      *opaque;
     ngx_http_request_t   *r;
 
-    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
-    if (r == NULL) {
-        (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_out object");
+    opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
+    if (opaque == NULL) {
         return -1;
     }
 
+    r = opaque->external;
+
     if (!JS_IsUndefined(setter) || !JS_IsUndefined(getter)) {
         (void) JS_ThrowTypeError(cx, "cannot define getter or setter");
         return -1;
@@ -9267,14 +9310,16 @@ ngx_http_qjs_headers_out_delete_property(JSContext *cx,
 {
     int                   ret;
     ngx_str_t             name;
+    ngx_js_opaque_t      *opaque;
     ngx_http_request_t   *r;
 
-    r = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
-    if (r == NULL) {
-        (void) JS_ThrowTypeError(cx, "\"this\" is not a headers_out object");
+    opaque = ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_HTTP_HEADERS_OUT);
+    if (opaque == NULL) {
         return -1;
     }
 
+    r = opaque->external;
+
     name.data = (u_char *) JS_AtomToCString(cx, prop);
     if (name.data == NULL) {
         return -1;
@@ -9390,23 +9435,94 @@ ngx_http_qjs_body_filter(ngx_http_request_t *r, ngx_http_js_loc_conf_t *jlcf,
 }
 
 
+static JSValue
+ngx_http_qjs_view_make(JSContext *cx, JSValueConst request,
+    JSClassID request_class, JSClassID view_class, void *external)
+{
+    JSValue           obj;
+    ngx_js_opaque_t  *parent, *opaque;
+
+    parent = ngx_qjs_opaque_get(cx, request, request_class);
+    if (parent == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    if (view_class == NGX_QJS_CLASS_ID_HTTP_FORM) {
+        obj = JS_NewObjectClass(cx, view_class);
+
+    } else {
+        obj = JS_NewObjectProtoClass(cx, JS_NULL, view_class);
+    }
+
+    if (JS_IsException(obj)) {
+        return obj;
+    }
+
+    opaque = ngx_qjs_opaque_alloc(cx, sizeof(ngx_js_opaque_t),
+                                 external != NULL ? external : parent->external,
+                                 parent->owner);
+    if (opaque == NULL) {
+        JS_FreeValue(cx, obj);
+        return JS_EXCEPTION;
+    }
+
+    JS_SetOpaque(obj, opaque);
+
+    return obj;
+}
+
+
+static JSValue
+ngx_http_qjs_variables_make(JSContext *cx, JSValueConst request,
+    JSClassID request_class, int type)
+{
+    JSValue                    obj;
+    ngx_js_opaque_t           *parent;
+    ngx_http_qjs_variables_t  *variables;
+
+    parent = ngx_qjs_opaque_get(cx, request, request_class);
+    if (parent == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_HTTP_VARS);
+    if (JS_IsException(obj)) {
+        return obj;
+    }
+
+    variables = (ngx_http_qjs_variables_t *) ngx_qjs_opaque_alloc(cx,
+                                   sizeof(ngx_http_qjs_variables_t),
+                                   parent->external, parent->owner);
+    if (variables == NULL) {
+        JS_FreeValue(cx, obj);
+        return JS_EXCEPTION;
+    }
+
+    variables->buffer_type = type == NGX_JS_BUFFER ? NGX_JS_BUFFER
+                                                  : NGX_JS_STRING;
+    JS_SetOpaque(obj, variables);
+
+    return obj;
+}
+
+
 static ngx_http_request_t *
-ngx_http_qjs_request(JSValueConst val)
+ngx_http_qjs_request(JSContext *cx, JSValueConst val)
 {
-    ngx_http_qjs_request_t  *req;
+    ngx_js_opaque_t  *opaque;
 
-    req = JS_GetOpaque(val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
-    if (req == NULL) {
+    opaque = ngx_qjs_opaque_get(cx, val, NGX_QJS_CLASS_ID_HTTP_REQUEST);
+    if (opaque == NULL) {
         return NULL;
     }
 
-    return req->opaque.external;
+    return opaque->external;
 }
 
 
 static JSValue
 ngx_http_qjs_request_make(JSContext *cx, ngx_int_t proto_id,
-    ngx_http_request_t *r)
+    ngx_http_request_t *r, ngx_qjs_owner_t *owner)
 {
     JSValue                  request;
     ngx_http_qjs_request_t  *req;
@@ -9416,13 +9532,13 @@ ngx_http_qjs_request_make(JSContext *cx, ngx_int_t proto_id,
         return JS_EXCEPTION;
     }
 
-    req = js_malloc(cx, sizeof(ngx_http_qjs_request_t));
+    req = (ngx_http_qjs_request_t *) ngx_qjs_opaque_alloc(cx,
+                                sizeof(ngx_http_qjs_request_t), r, owner);
     if (req == NULL) {
         JS_FreeValue(cx, request);
-        return JS_ThrowOutOfMemory(cx);
+        return JS_EXCEPTION;
     }
 
-    req->opaque.external = r;
     req->args = JS_UNDEFINED;
     req->request_body = JS_UNDEFINED;
     req->response_body = JS_UNDEFINED;
@@ -9462,7 +9578,7 @@ ngx_http_qjs_request_finalizer(JSRuntime *rt, JSValue val)
     JS_FreeValueRT(rt, req->request_body);
     JS_FreeValueRT(rt, req->response_body);
 
-    js_free_rt(rt, req);
+    ngx_qjs_opaque_free(rt, &req->opaque);
 }
 
 
@@ -9476,7 +9592,7 @@ ngx_http_qjs_periodic_finalizer(JSRuntime *rt, JSValue val)
         return;
     }
 
-    js_free_rt(rt, req);
+    ngx_qjs_opaque_free(rt, &req->opaque);
 }
 
 
@@ -9574,7 +9690,7 @@ ngx_engine_qjs_clone(ngx_js_ctx_t *ctx, ngx_js_loc_conf_t *cf,
     }
 
     ngx_qjs_arg(ctx->args[0]) = ngx_http_qjs_request_make(cx, proto_id,
-                                                          external);
+                                                        external, NULL);
     if (JS_IsException(ngx_qjs_arg(ctx->args[0]))) {
         goto failed;
     }
index a85251b988eb6f065eb4db80174209c8eb6ce948..78cbe5b63845c9d936aa01025abdb6e508ac9edb 100644 (file)
@@ -1301,6 +1301,10 @@ ngx_qjs_detach_ctx(ngx_js_ctx_t *ctx, JSContext *cx)
         opaque = JS_GetOpaque(ngx_qjs_arg(ctx->args[0]), class_id);
 
         if (opaque != NULL) {
+            if (opaque->owner != NULL) {
+                opaque->owner->alive = 0;
+            }
+
             opaque->external = NULL;
         }
     }
@@ -1832,6 +1836,103 @@ ngx_qjs_value_bytes_copy(JSContext *cx, JSValueConst value, ngx_str_t *str)
 }
 
 
+ngx_js_opaque_t *
+ngx_qjs_opaque_alloc(JSContext *cx, size_t size, void *external,
+    ngx_qjs_owner_t *owner)
+{
+    ngx_js_opaque_t  *opaque;
+
+    if (owner != NULL && owner->runtime != JS_GetRuntime(cx)) {
+        JS_ThrowTypeError(cx, "native object belongs to a different runtime");
+        return NULL;
+    }
+
+    opaque = js_malloc(cx, size);
+    if (opaque == NULL) {
+        return NULL;
+    }
+
+    if (owner == NULL) {
+        owner = js_mallocz(cx, sizeof(ngx_qjs_owner_t));
+        if (owner == NULL) {
+            js_free(cx, opaque);
+            return NULL;
+        }
+
+        owner->alive = 1;
+        owner->runtime = JS_GetRuntime(cx);
+    }
+
+    owner->references++;
+    opaque->external = external;
+    opaque->owner = owner;
+
+    return opaque;
+}
+
+
+ngx_js_opaque_t *
+ngx_qjs_opaque_get(JSContext *cx, JSValueConst value, JSClassID class_id)
+{
+    ngx_js_opaque_t  *opaque;
+
+    opaque = JS_GetOpaque2(cx, value, class_id);
+    if (opaque == NULL) {
+        return NULL;
+    }
+
+    if (opaque->owner != NULL
+        && opaque->owner->runtime != JS_GetRuntime(cx))
+    {
+        JS_ThrowTypeError(cx, "native object belongs to a different runtime");
+        return NULL;
+    }
+
+    if (opaque->external == NULL
+        || (opaque->owner != NULL && !opaque->owner->alive))
+    {
+        JS_ThrowTypeError(cx, "native object is no longer valid");
+        return NULL;
+    }
+
+    return opaque;
+}
+
+
+void
+ngx_qjs_owner_release(JSRuntime *rt, ngx_qjs_owner_t *owner)
+{
+    if (owner != NULL) {
+        njs_assert(owner->runtime == rt);
+        njs_assert(owner->references != 0);
+
+        if (--owner->references == 0) {
+            js_free_rt(rt, owner);
+        }
+    }
+}
+
+
+void
+ngx_qjs_opaque_free(JSRuntime *rt, ngx_js_opaque_t *opaque)
+{
+    ngx_qjs_owner_release(rt, opaque->owner);
+    js_free_rt(rt, opaque);
+}
+
+
+void
+ngx_qjs_opaque_finalizer(JSRuntime *rt, JSValue value)
+{
+    ngx_js_opaque_t  *opaque;
+
+    opaque = JS_GetOpaque(value, JS_GetClassID(value));
+    if (opaque != NULL) {
+        ngx_qjs_opaque_free(rt, opaque);
+    }
+}
+
+
 static void
 ngx_qjs_timer_handler(ngx_event_t *ev)
 {
index aa866d42d8376b0c62b00edf2867916f7864eff1..2b847b90ecae06364d1197ba8b6740ecb5b0c4bc 100644 (file)
@@ -235,8 +235,20 @@ struct ngx_js_ctx_s {
 };
 
 
+#if (NJS_HAVE_QUICKJS)
+typedef struct {
+    JSRuntime                   *runtime;
+    ngx_uint_t                   references;
+    unsigned                     alive:1;
+} ngx_qjs_owner_t;
+#endif
+
+
 typedef struct {
     void                        *external;
+#if (NJS_HAVE_QUICKJS)
+    ngx_qjs_owner_t              *owner;
+#endif
 } ngx_js_opaque_t;
 
 
@@ -408,6 +420,13 @@ ngx_int_t ngx_qjs_bytes_copy(JSContext *cx, ngx_str_t *dst,
     const ngx_str_t *src);
 ngx_int_t ngx_qjs_value_bytes_copy(JSContext *cx, JSValueConst value,
     ngx_str_t *str);
+ngx_js_opaque_t *ngx_qjs_opaque_alloc(JSContext *cx, size_t size,
+    void *external, ngx_qjs_owner_t *owner);
+ngx_js_opaque_t *ngx_qjs_opaque_get(JSContext *cx, JSValueConst value,
+    JSClassID class_id);
+void ngx_qjs_owner_release(JSRuntime *rt, ngx_qjs_owner_t *owner);
+void ngx_qjs_opaque_free(JSRuntime *rt, ngx_js_opaque_t *opaque);
+void ngx_qjs_opaque_finalizer(JSRuntime *rt, JSValue value);
 ngx_int_t ngx_qjs_header_value(JSContext *cx, ngx_pool_t *pool,
     JSValueConst val, ngx_str_t *dst);
 
index bd6e6a794744c219d2b7bfeaabc8ea2904518045..334c04f8b5c867435849415d1844f2731b4ef8b1 100644 (file)
@@ -91,6 +91,12 @@ typedef struct {
     JSValue          callbacks[NGX_JS_EVENT_MAX];
 } ngx_stream_qjs_session_t;
 
+
+typedef struct {
+    ngx_js_opaque_t  opaque;
+    ngx_uint_t       buffer_type;
+} ngx_stream_qjs_variables_t;
+
 #endif
 
 #define ngx_stream_pending(ctx)                                               \
@@ -191,7 +197,10 @@ static ngx_int_t ngx_stream_qjs_run_event(ngx_stream_session_t *s,
 static ngx_int_t ngx_stream_qjs_body_filter(ngx_stream_session_t *s,
     ngx_stream_js_ctx_t *ctx, ngx_chain_t *in, ngx_uint_t from_upstream);
 
-static ngx_stream_session_t *ngx_stream_qjs_session(JSValueConst val);
+static ngx_stream_session_t *ngx_stream_qjs_session(JSContext *cx,
+    JSValueConst val);
+static JSValue ngx_stream_qjs_variables_make(JSContext *cx,
+    JSValueConst session, JSClassID class_id, int type);
 static JSValue ngx_stream_qjs_session_make(JSContext *cx, ngx_int_t proto_id,
     ngx_stream_session_t *s);
 static void ngx_stream_qjs_session_mark(JSRuntime *rt, JSValueConst val,
@@ -902,7 +911,7 @@ static JSClassDef ngx_stream_qjs_flags_class = {
 
 static JSClassDef ngx_stream_qjs_variables_class = {
     "Variables",
-    .finalizer = NULL,
+    .finalizer = ngx_qjs_opaque_finalizer,
     .exotic = & (JSClassExoticMethods) {
         .get_own_property = ngx_stream_qjs_variables_own_property,
         .set_property = ngx_stream_qjs_variables_set_property,
@@ -2137,9 +2146,9 @@ ngx_stream_qjs_ext_done(JSContext *cx, JSValueConst this_val, int argc,
     ngx_stream_js_ctx_t   *ctx;
     ngx_stream_session_t  *s;
 
-    s = ngx_stream_qjs_session(this_val);
+    s = ngx_stream_qjs_session(cx, this_val);
     if (s == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a session object");
+        return JS_EXCEPTION;
     }
 
     status = (ngx_int_t) magic;
@@ -2184,9 +2193,9 @@ ngx_stream_qjs_ext_log(JSContext *cx, JSValueConst this_val, int argc,
     const char            *msg;
     ngx_stream_session_t  *s;
 
-    s = ngx_stream_qjs_session(this_val);
+    s = ngx_stream_qjs_session(cx, this_val);
     if (s == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a session object");
+        return JS_EXCEPTION;
     }
 
     for (n = 0; n < argc; n++) {
@@ -2291,9 +2300,10 @@ ngx_stream_qjs_ext_on(JSContext *cx, JSValueConst this_val, int argc,
     ngx_stream_qjs_session_t      *ses;
     const ngx_stream_qjs_event_t  *e;
 
-    ses = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_STREAM_SESSION);
+    ses = (ngx_stream_qjs_session_t *) ngx_qjs_opaque_get(cx, this_val,
+                                               NGX_QJS_CLASS_ID_STREAM_SESSION);
     if (ses == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a session object");
+        return JS_EXCEPTION;
     }
 
     s = ses->opaque.external;
@@ -2304,6 +2314,10 @@ ngx_stream_qjs_ext_on(JSContext *cx, JSValueConst this_val, int argc,
         return JS_EXCEPTION;
     }
 
+    if (ngx_stream_qjs_session(cx, this_val) == NULL) {
+        return JS_EXCEPTION;
+    }
+
     if (JS_IsFunction(cx, ngx_qjs_arg(ctx->events[e->id].function))) {
         return JS_ThrowTypeError(cx, "event handler \"%s\" is already set",
                                  e->name.data);
@@ -2330,9 +2344,9 @@ ngx_stream_qjs_ext_off(JSContext *cx, JSValueConst this_val, int argc,
     ngx_stream_session_t          *s;
     const ngx_stream_qjs_event_t  *e;
 
-    s = ngx_stream_qjs_session(this_val);
+    s = ngx_stream_qjs_session(cx, this_val);
     if (s == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a session object");
+        return JS_EXCEPTION;
     }
 
     ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);
@@ -2342,6 +2356,10 @@ ngx_stream_qjs_ext_off(JSContext *cx, JSValueConst this_val, int argc,
         return JS_EXCEPTION;
     }
 
+    if (ngx_stream_qjs_session(cx, this_val) == NULL) {
+        return JS_EXCEPTION;
+    }
+
     ngx_qjs_arg(ctx->events[e->id].function) = JS_NULL;
     ctx->events[e->id].data_type = NGX_JS_UNSET;
 
@@ -2353,24 +2371,8 @@ static JSValue
 ngx_stream_qjs_ext_periodic_variables(JSContext *cx,
     JSValueConst this_val, int type)
 {
-    JSValue                    obj;
-    ngx_stream_qjs_session_t  *ses;
-
-    ses = JS_GetOpaque(this_val, NGX_QJS_CLASS_ID_STREAM_PERIODIC);
-    if (ses == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a periodic object");
-    }
-
-    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_STREAM_VARS);
-
-    /*
-     * Using lowest bit of the pointer to store the buffer type.
-     */
-    type = (type == NGX_JS_BUFFER) ? 1 : 0;
-    JS_SetOpaque(obj, (void *) ((uintptr_t) ses->opaque.external
-                              | (uintptr_t) type));
-
-    return obj;
+    return ngx_stream_qjs_variables_make(cx, this_val,
+                                        NGX_QJS_CLASS_ID_STREAM_PERIODIC, type);
 }
 
 
@@ -2380,9 +2382,9 @@ ngx_stream_qjs_ext_remote_address(JSContext *cx, JSValueConst this_val)
     ngx_connection_t      *c;
     ngx_stream_session_t  *s;
 
-    s = ngx_stream_qjs_session(this_val);
+    s = ngx_stream_qjs_session(cx, this_val);
     if (s == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a session object");
+        return JS_EXCEPTION;
     }
 
     c = s->connection;
@@ -2406,9 +2408,9 @@ ngx_stream_qjs_ext_send(JSContext *cx, JSValueConst this_val, int argc,
     ngx_stream_js_ctx_t   *ctx;
     ngx_stream_session_t  *s;
 
-    s = ngx_stream_qjs_session(this_val);
+    s = ngx_stream_qjs_session(cx, this_val);
     if (s == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a session object");
+        return JS_EXCEPTION;
     }
 
     c = s->connection;
@@ -2598,9 +2600,9 @@ ngx_stream_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val,
     ngx_js_ctx_t          *ctx;
     ngx_stream_session_t  *s;
 
-    s = ngx_stream_qjs_session(this_val);
+    s = ngx_stream_qjs_session(cx, this_val);
     if (s == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a session object");
+        return JS_EXCEPTION;
     }
 
     ctx = ngx_stream_get_module_ctx(s, ngx_stream_js_module);
@@ -2615,23 +2617,8 @@ ngx_stream_qjs_ext_set_return_value(JSContext *cx, JSValueConst this_val,
 static JSValue
 ngx_stream_qjs_ext_variables(JSContext *cx, JSValueConst this_val, int type)
 {
-    JSValue                obj;
-    ngx_stream_session_t  *s;
-
-    s = ngx_stream_qjs_session(this_val);
-    if (s == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a session object");
-    }
-
-    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_STREAM_VARS);
-
-    /*
-     * Using lowest bit of the pointer to store the buffer type.
-     */
-    type = (type == NGX_JS_BUFFER) ? 1 : 0;
-    JS_SetOpaque(obj, (void *) ((uintptr_t) s | (uintptr_t) type));
-
-    return obj;
+    return ngx_stream_qjs_variables_make(cx, this_val,
+                                        NGX_QJS_CLASS_ID_STREAM_SESSION, type);
 }
 
 
@@ -2648,9 +2635,9 @@ ngx_stream_qjs_ext_js_var_names(JSContext *cx, JSValueConst this_val, int argc,
     ngx_stream_variable_t        *v;
     ngx_stream_core_main_conf_t  *cmcf;
 
-    s = ngx_stream_qjs_session(this_val);
+    s = ngx_stream_qjs_session(cx, this_val);
     if (s == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a session object");
+        return JS_EXCEPTION;
     }
 
     prefix = NULL;
@@ -2716,9 +2703,9 @@ ngx_stream_qjs_ext_uint(JSContext *cx, JSValueConst this_val, int offset)
     ngx_uint_t            *field;
     ngx_stream_session_t  *s;
 
-    s = ngx_stream_qjs_session(this_val);
+    s = ngx_stream_qjs_session(cx, this_val);
     if (s == NULL) {
-        return JS_ThrowTypeError(cx, "\"this\" is not a session object");
+        return JS_EXCEPTION;
     }
 
     field = (ngx_uint_t *) ((u_char *) s + offset);
@@ -2746,19 +2733,19 @@ ngx_stream_qjs_variables_own_property(JSContext *cx,
     ngx_str_t                     name, name_lc;
     ngx_uint_t                    key;
     ngx_stream_session_t         *s;
+    ngx_stream_qjs_variables_t   *variables;
     ngx_stream_variable_value_t  *vv;
     u_char                        storage[64];
 
-    s = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_STREAM_VARS);
-
-    buffer_type = ((uintptr_t) s & 1) ? NGX_JS_BUFFER : NGX_JS_STRING;
-    s = (ngx_stream_session_t *) ((uintptr_t) s & ~(uintptr_t) 1);
-
-    if (s == NULL) {
-        (void) JS_ThrowTypeError(cx, "\"this\" is not a session object");
+    variables = (ngx_stream_qjs_variables_t *) ngx_qjs_opaque_get(cx, obj,
+                                                 NGX_QJS_CLASS_ID_STREAM_VARS);
+    if (variables == NULL) {
         return -1;
     }
 
+    buffer_type = variables->buffer_type;
+    s = variables->opaque.external;
+
     name.data = (u_char *) JS_AtomToCString(cx, prop);
     if (name.data == NULL) {
         return -1;
@@ -2784,6 +2771,10 @@ ngx_stream_qjs_variables_own_property(JSContext *cx,
 
     vv = ngx_stream_get_variable(s, &name_lc, key);
     JS_FreeCString(cx, (char *) name.data);
+    if (ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_STREAM_VARS) == NULL) {
+        return -1;
+    }
+
     if (vv == NULL || vv->not_found) {
         return 0;
     }
@@ -2807,19 +2798,19 @@ ngx_stream_qjs_variables_set_property(JSContext *cx, JSValueConst obj,
     ngx_uint_t                    key;
     ngx_stream_session_t         *s;
     ngx_stream_variable_t        *v;
+    ngx_stream_qjs_variables_t   *variables;
     ngx_stream_variable_value_t  *vv;
     ngx_stream_core_main_conf_t  *cmcf;
     u_char                        storage[64];
 
-    s = JS_GetOpaque(obj, NGX_QJS_CLASS_ID_STREAM_VARS);
-
-    s = (ngx_stream_session_t *) ((uintptr_t) s & ~(uintptr_t) 1);
-
-    if (s == NULL) {
-        (void) JS_ThrowTypeError(cx, "\"this\" is not a session object");
+    variables = (ngx_stream_qjs_variables_t *) ngx_qjs_opaque_get(cx, obj,
+                                                 NGX_QJS_CLASS_ID_STREAM_VARS);
+    if (variables == NULL) {
         return -1;
     }
 
+    s = variables->opaque.external;
+
     name.data = (u_char *) JS_AtomToCString(cx, prop);
     if (name.data == NULL) {
         return -1;
@@ -2855,6 +2846,10 @@ ngx_stream_qjs_variables_set_property(JSContext *cx, JSValueConst obj,
         return -1;
     }
 
+    if (ngx_qjs_opaque_get(cx, obj, NGX_QJS_CLASS_ID_STREAM_VARS) == NULL) {
+        return -1;
+    }
+
     if (v->set_handler != NULL) {
         vv = ngx_pcalloc(s->connection->pool,
                          sizeof(ngx_stream_variable_value_t));
@@ -3008,17 +3003,51 @@ ngx_stream_qjs_body_filter(ngx_stream_session_t *s, ngx_stream_js_ctx_t *ctx,
 }
 
 
+static JSValue
+ngx_stream_qjs_variables_make(JSContext *cx, JSValueConst session,
+    JSClassID class_id, int type)
+{
+    JSValue                      obj;
+    ngx_js_opaque_t             *parent;
+    ngx_stream_qjs_variables_t  *variables;
+
+    parent = ngx_qjs_opaque_get(cx, session, class_id);
+    if (parent == NULL) {
+        return JS_EXCEPTION;
+    }
+
+    obj = JS_NewObjectProtoClass(cx, JS_NULL, NGX_QJS_CLASS_ID_STREAM_VARS);
+    if (JS_IsException(obj)) {
+        return obj;
+    }
+
+    variables = (ngx_stream_qjs_variables_t *) ngx_qjs_opaque_alloc(cx,
+                                   sizeof(ngx_stream_qjs_variables_t),
+                                   parent->external, parent->owner);
+    if (variables == NULL) {
+        JS_FreeValue(cx, obj);
+        return JS_EXCEPTION;
+    }
+
+    variables->buffer_type = type == NGX_JS_BUFFER ? NGX_JS_BUFFER
+                                                  : NGX_JS_STRING;
+    JS_SetOpaque(obj, variables);
+
+    return obj;
+}
+
+
 static ngx_stream_session_t *
-ngx_stream_qjs_session(JSValueConst val)
+ngx_stream_qjs_session(JSContext *cx, JSValueConst val)
 {
-    ngx_stream_qjs_session_t  *ses;
+    ngx_js_opaque_t  *opaque;
 
-    ses = JS_GetOpaque(val, NGX_QJS_CLASS_ID_STREAM_SESSION);
-    if (ses == NULL) {
+    opaque = ngx_qjs_opaque_get(cx, val, NGX_QJS_CLASS_ID_STREAM_SESSION);
+    if (opaque == NULL) {
         return NULL;
     }
 
-    return ses->opaque.external;
+    return opaque->external;
 }
 
 
@@ -3035,14 +3064,13 @@ ngx_stream_qjs_session_make(JSContext *cx, ngx_int_t proto_id,
         return JS_EXCEPTION;
     }
 
-    ses = js_malloc(cx, sizeof(ngx_stream_qjs_session_t));
+    ses = (ngx_stream_qjs_session_t *) ngx_qjs_opaque_alloc(cx,
+                                  sizeof(ngx_stream_qjs_session_t), s, NULL);
     if (ses == NULL) {
         JS_FreeValue(cx, session);
-        return JS_ThrowOutOfMemory(cx);
+        return JS_EXCEPTION;
     }
 
-    ses->opaque.external = s;
-
     for (i = 0; i < NGX_JS_EVENT_MAX; i++) {
         ses->callbacks[i] = JS_UNDEFINED;
     }
@@ -3084,7 +3112,7 @@ ngx_stream_qjs_session_finalizer(JSRuntime *rt, JSValue val)
         JS_FreeValueRT(rt, ses->callbacks[i]);
     }
 
-    js_free_rt(rt, ses);
+    ngx_qjs_opaque_free(rt, &ses->opaque);
 }
 
 
@@ -3098,7 +3126,7 @@ ngx_stream_qjs_periodic_finalizer(JSRuntime *rt, JSValue val)
         return;
     }
 
-    js_free_rt(rt, ses);
+    ngx_qjs_opaque_free(rt, &ses->opaque);
 }
 
 
diff --git a/nginx/t/js_http_lifetime_scopes.t b/nginx/t/js_http_lifetime_scopes.t
new file mode 100644 (file)
index 0000000..c9ae9c1
--- /dev/null
@@ -0,0 +1,199 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) F5, Inc.
+
+# Tests for QuickJS lifetime scopes during redirects and periodic cleanup.
+
+###############################################################################
+
+use warnings;
+use strict;
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http rewrite/)
+    ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+worker_processes 1;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_context_reuse 1;
+    js_import test.js;
+    js_var $test_value original;
+    js_shared_dict_zone zone=scopes:32k;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /engine {
+            js_content test.engine;
+        }
+
+        location /source {
+            js_import source.js;
+            js_content source.content;
+        }
+
+        location /destination {
+            js_import destination.js;
+            js_content destination.content;
+        }
+
+        location @periodic {
+            js_periodic test.periodic interval=40ms;
+        }
+
+        location @forced {
+            js_periodic test.forced interval=50ms;
+        }
+
+        location /results {
+            js_content test.results;
+        }
+    }
+}
+
+EOF
+
+$t->write_file('source.js', <<'EOF');
+    let headers;
+
+    function content(r) {
+        let expired = headers === undefined;
+        if (headers !== undefined) {
+            try {
+                headers.host;
+            } catch (e) {
+                expired = e instanceof TypeError;
+            }
+        }
+
+        headers = r.headersIn;
+        njs.on('exit', () => {
+            ngx.shared.scopes.set('source_exit',
+                                  String(headers.host === 'localhost'));
+        });
+        r.internalRedirect('/destination?source=' + expired);
+    }
+
+    export default {content};
+EOF
+
+$t->write_file('destination.js', <<'EOF');
+    let variables;
+
+    function content(r) {
+        let expired = variables === undefined;
+        if (variables !== undefined) {
+            try {
+                variables.test_value;
+            } catch (e) {
+                expired = e instanceof TypeError;
+            }
+        }
+
+        variables = r.variables;
+        njs.on('exit', () => {
+            ngx.shared.scopes.set('destination_exit',
+                                  String(variables.test_value === 'original'));
+        });
+        r.return(200, r.args.source + ':' + expired);
+    }
+
+    export default {content};
+EOF
+
+$t->write_file('test.js', <<'EOF');
+    let previous, previousForced;
+
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
+    function check(value) {
+        try {
+            value.test_value;
+        } catch (e) {
+            return e instanceof TypeError;
+        }
+
+        return false;
+    }
+
+    function periodic(s) {
+        if (njs.engine !== 'QuickJS') {
+            return;
+        }
+
+        if (previous !== undefined) {
+            ngx.shared.scopes.set('periodic', String(check(previous)));
+        }
+
+        previous = s.variables;
+    }
+
+    function forced(s) {
+        if (njs.engine !== 'QuickJS') {
+            return;
+        }
+
+        if (previousForced === undefined) {
+            previousForced = s.rawVariables;
+            setTimeout(() => {}, 100000);
+            return;
+        }
+
+        ngx.shared.scopes.set('forced', String(check(previousForced)));
+    }
+
+    function results(r) {
+        const d = ngx.shared.scopes;
+        r.return(200, ['periodic', 'forced', 'source_exit', 'destination_exit']
+                      .map(name => d.get(name)).join(':'));
+    }
+
+    export default {engine, periodic, forced, results};
+EOF
+
+$t->try_run('no njs available');
+
+plan(skip_all => 'QuickJS-specific lifetime tests')
+    if http_get('/engine') =~ /njs$/m;
+
+$t->plan(3);
+
+###############################################################################
+
+like(http_get('/source'), qr/true:true$/, 'initial redirect scopes');
+like(http_get('/source'), qr/true:true$/, 'old scopes expire independently');
+
+my $result;
+for (1 .. 50) {
+    $result = http_get('/results');
+    last if $result =~ /true:true:true:true$/;
+    select undef, undef, undef, 0.02;
+}
+
+like($result, qr/true:true:true:true$/,
+    'periodic expiry and synchronous exit-hook access');
+
+###############################################################################
diff --git a/nginx/t/js_http_object_lifetime.t b/nginx/t/js_http_object_lifetime.t
new file mode 100644 (file)
index 0000000..97dba29
--- /dev/null
@@ -0,0 +1,227 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) F5, Inc.
+
+# Tests for QuickJS HTTP object lifetime after request cleanup.
+
+###############################################################################
+
+use warnings;
+use strict;
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/http rewrite/)
+    ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+worker_processes 1;
+
+events {
+}
+
+http {
+    %%TEST_GLOBALS_HTTP%%
+
+    js_context_reuse 1;
+    js_import test.js;
+    js_var $test_value original;
+
+    server {
+        listen       127.0.0.1:8080;
+        server_name  localhost;
+
+        location /engine {
+            js_content test.engine;
+        }
+
+        location /a {
+            js_content test.content;
+        }
+
+        location /child {
+            return 200 'child';
+        }
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<'EOF');
+    let saved, snapshots;
+
+    function engine(r) {
+        r.return(200, njs.engine);
+    }
+
+    const checks = {
+        headers_in: () => saved.host,
+        headers_in_keys: () => Object.keys(saved),
+        headers_out: () => saved['X-Test'],
+        headers_out_set: () => { saved['X-Test'] = 'changed'; },
+        headers_out_delete: () => { delete saved['X-Test']; },
+        headers_out_define: () => {
+            Object.defineProperty(saved, 'X-Test', {value: 'changed'});
+        },
+        headers_out_keys: () => Object.keys(saved),
+        variables: () => saved.test_value,
+        variables_set: () => { saved.test_value = 'changed'; },
+        raw_variables: () => saved.test_value,
+        form: () => saved.get('x'),
+        form_all: () => saved.getAll('x'),
+        form_has: () => saved.has('x'),
+        form_each: () => saved.forEach(() => {}),
+        form_files: () => saved.hasFiles(),
+        form_file: () => saved.get('upload'),
+        request: () => saved.uri,
+        args_hot: () => saved.args,
+        body_cold: () => saved.requestText,
+        body_hot: () => saved.requestText,
+        body_switch: () => saved.requestBuffer,
+        read_text: () => saved.readRequestText(),
+        read_form: () => saved.readRequestForm(),
+        reply: () => saved.status,
+        reply_callback: () => saved.status,
+        reply_headers: () => saved.headersOut,
+        reply_header_view: () => saved['Content-Type'],
+        reply_variables: () => saved.test_value,
+        reply_body: () => saved.responseBuffer
+    };
+
+    async function content(r) {
+        const name = r.args.case;
+        if (r.args.keep) {
+            snapshots = {args: r.args, uri: r.uri};
+
+            if (name.startsWith('headers_in')) {
+                saved = r.headersIn;
+            } else if (name.startsWith('headers_out')) {
+                r.headersOut['X-Test'] = 'original';
+                saved = r.headersOut;
+            } else if (name.startsWith('variables')) {
+                saved = r.variables;
+            } else if (name === 'raw_variables') {
+                saved = r.rawVariables;
+                snapshots.buffer = saved.test_value;
+            } else if (name.startsWith('form')) {
+                saved = await r.readRequestForm();
+                snapshots.form = saved.get('x');
+                if (name === 'form_file') {
+                    snapshots.filename = saved.get('upload').name;
+                }
+            } else if (name.startsWith('reply')) {
+                if (name === 'reply_callback') {
+                    saved = await new Promise(resolve => {
+                        r.subrequest('/child', resolve);
+                    });
+                } else {
+                    saved = await r.subrequest('/child');
+                }
+
+                if (saved.status !== 200 || saved.headersOut === undefined)
+                {
+                    throw new Error('reply expired before parent cleanup');
+                }
+
+                snapshots.reply = saved.responseText;
+                if (name === 'reply_header_view') {
+                    saved = saved.headersOut;
+                } else if (name === 'reply_variables') {
+                    saved = saved.variables;
+                }
+            } else {
+                saved = r;
+                if (name === 'body_hot' || name === 'body_switch') {
+                    snapshots.body = r.requestText;
+                }
+            }
+
+            r.return(200, 'kept');
+            return;
+        }
+
+        let expired = false;
+        try {
+            checks[name]();
+        } catch (e) {
+            expired = e instanceof TypeError;
+        }
+
+        const independent = snapshots.args.keep === '1'
+                            && snapshots.uri === '/a'
+                            && (snapshots.form === undefined
+                                || snapshots.form === 'original')
+                            && (snapshots.body === undefined
+                                || snapshots.body === 'x=original')
+                            && (snapshots.filename === undefined
+                                || snapshots.filename === 'test.txt')
+                            && (snapshots.reply === undefined
+                                || snapshots.reply === 'child')
+                            && (snapshots.buffer === undefined
+                                || snapshots.buffer.toString() === 'original');
+
+        saved = null;
+        snapshots = null;
+        r.return(200, expired + ':' + independent);
+    }
+
+    export default {content, engine};
+
+EOF
+
+my @cases = qw/
+    headers_in headers_in_keys
+    headers_out headers_out_set headers_out_delete headers_out_define
+    headers_out_keys variables variables_set raw_variables
+    form form_all form_has form_each form_files form_file
+    request args_hot body_cold body_hot body_switch read_text read_form
+    reply reply_callback reply_headers reply_header_view reply_variables
+    reply_body
+/;
+
+@cases = grep { $_ eq $ENV{NJS_LIFETIME_CASE} } @cases
+    if defined $ENV{NJS_LIFETIME_CASE};
+
+$t->try_run('no njs available');
+
+plan(skip_all => 'QuickJS-specific lifetime tests')
+    if http_get('/engine') =~ /njs$/m;
+
+$t->plan(2 * @cases);
+
+###############################################################################
+
+for my $case (@cases) {
+    my $type = 'application/x-www-form-urlencoded';
+    my $body = 'x=original';
+
+    if ($case eq 'form_file') {
+        $type = 'multipart/form-data; boundary=A';
+        $body = "--A\r\nContent-Disposition: form-data; name=\"x\"\r\n\r\n"
+                . "original\r\n--A\r\n"
+                . "Content-Disposition: form-data; name=\"upload\"; "
+                . "filename=\"test.txt\"\r\n\r\npayload\r\n--A--\r\n";
+    }
+
+    like(http("POST /a?keep=1&case=$case HTTP/1.0\r\n"
+              . "Host: localhost\r\n"
+              . "Content-Type: $type\r\n"
+              . "Content-Length: " . length($body) . "\r\n\r\n$body"),
+         qr/kept$/, "$case before cleanup");
+    like(http_get("/a?case=$case"), qr/true:true$/,
+         "$case expires without invalidating extracted values");
+}
+
+###############################################################################
diff --git a/nginx/t/stream_js_object_lifetime.t b/nginx/t/stream_js_object_lifetime.t
new file mode 100644 (file)
index 0000000..82901db
--- /dev/null
@@ -0,0 +1,207 @@
+#!/usr/bin/perl
+
+# (C) Dmitry Volyntsev
+# (C) F5, Inc.
+
+# Tests for QuickJS stream objects after session and periodic cleanup.
+
+###############################################################################
+
+use warnings;
+use strict;
+use Test::More;
+
+BEGIN { use FindBin; chdir($FindBin::Bin); }
+
+use lib 'lib';
+use Test::Nginx;
+use Test::Nginx::Stream qw/ stream /;
+
+###############################################################################
+
+select STDERR; $| = 1;
+select STDOUT; $| = 1;
+
+my $t = Test::Nginx->new()->has(qw/stream stream_return/)
+    ->write_file_expand('nginx.conf', <<'EOF');
+
+%%TEST_GLOBALS%%
+
+daemon off;
+worker_processes 1;
+
+events {
+}
+
+stream {
+    %%TEST_GLOBALS_STREAM%%
+
+    js_context_reuse 1;
+    js_import test.js;
+    js_var $test_value original;
+    js_var $result;
+    js_set $engine test.engine;
+    js_shared_dict_zone zone=scopes:32k;
+
+    server {
+        listen      127.0.0.1:8081;
+        js_preread  test.preread;
+        return      $result;
+    }
+
+    server {
+        listen  127.0.0.1:8082;
+        return  $engine;
+    }
+
+    server {
+        listen       127.0.0.1:8083;
+        js_periodic  test.periodic interval=40ms;
+        return       OK;
+    }
+
+    server {
+        listen      127.0.0.1:8084;
+        js_preread  test.results;
+        return      $result;
+    }
+}
+
+EOF
+
+$t->write_file('test.js', <<'EOF');
+    let saved, snapshot, flags, address, previous;
+
+    function engine() {
+        return njs.engine;
+    }
+
+    const checks = {
+        variables: () => saved.test_value,
+        variables_set: () => { saved.test_value = 'changed'; },
+        variables_define: () => {
+            Object.defineProperty(saved, 'test_value', {value: 'changed'});
+        },
+        raw_variables: () => saved.test_value,
+        session: () => saved.remoteAddress,
+        status: () => saved.status,
+        on: () => saved.on('upload', () => {}),
+        off: () => saved.off('upload'),
+        done: () => saved.done(),
+        send: () => saved.sendUpstream('x'),
+        return_value: () => saved.setReturnValue('x'),
+        var_names: () => saved.jsVarNames()
+    };
+
+    function preread(s) {
+        s.on('upload', (data, f) => {
+            if (data.length === 0) {
+                return;
+            }
+
+            const parts = data.split(':');
+            const action = parts[0], name = parts[1];
+            if (action === 'keep') {
+                snapshot = s.rawVariables.test_value;
+                flags = f;
+                address = s.remoteAddress;
+                if (name.startsWith('variables')) {
+                    saved = s.variables;
+                } else if (name === 'raw_variables') {
+                    saved = s.rawVariables;
+                } else {
+                    saved = s;
+                }
+
+                s.variables.result = 'kept';
+            } else {
+                let expired = false;
+                try {
+                    checks[name]();
+                } catch (e) {
+                    expired = e instanceof TypeError;
+                }
+
+                const independent = snapshot.toString() === 'original'
+                                    && flags.from_upstream === false
+                                    && address === '127.0.0.1';
+                saved = null;
+                snapshot = null;
+                flags = null;
+                s.variables.result = expired + ':' + independent;
+            }
+
+            s.done();
+        });
+    }
+
+    function periodic(s) {
+        if (njs.engine !== 'QuickJS'
+            || ngx.shared.scopes.get('enabled') !== '1')
+        {
+            return;
+        }
+
+        if (previous !== undefined) {
+            let expired = false;
+            try {
+                previous.test_value;
+            } catch (e) {
+                expired = e instanceof TypeError;
+            }
+
+            ngx.shared.scopes.set('periodic', String(expired));
+        }
+
+        previous = s.variables;
+    }
+
+    function results(s) {
+        s.on('upload', data => {
+            if (data.length === 0) {
+                return;
+            }
+
+            ngx.shared.scopes.set('enabled', '1');
+            s.variables.result = ngx.shared.scopes.get('periodic') || '-';
+            s.done();
+        });
+    }
+
+    export default {engine, preread, periodic, results};
+EOF
+
+my @cases = qw/variables variables_set variables_define raw_variables
+               session status on off done send return_value var_names/;
+@cases = grep { $_ eq $ENV{NJS_LIFETIME_CASE} } @cases
+    if defined $ENV{NJS_LIFETIME_CASE};
+my $periodic = !defined $ENV{NJS_LIFETIME_CASE}
+               || $ENV{NJS_LIFETIME_CASE} eq 'periodic';
+
+$t->try_run('no stream njs available');
+plan(skip_all => 'QuickJS-specific lifetime tests')
+    if stream('127.0.0.1:' . port(8082))->read() =~ /njs$/;
+$t->plan(2 * @cases + ($periodic ? 1 : 0));
+
+###############################################################################
+
+for my $case (@cases) {
+    is(stream('127.0.0.1:' . port(8081))->io("keep:$case"), 'kept',
+       "$case before cleanup");
+    is(stream('127.0.0.1:' . port(8081))->io("read:$case"), 'true:true',
+       "$case expires without invalidating extracted values");
+}
+
+if ($periodic) {
+    my $result;
+    for (1 .. 50) {
+        $result = stream('127.0.0.1:' . port(8084))->io('results');
+        last if $result eq 'true';
+        select undef, undef, undef, 0.02;
+    }
+
+    is($result, 'true',
+       'periodic variables expire with their originating session');
+}
+
+###############################################################################