Parcourir la source

fix(runner): add smartbotic.log.debug and repair binary HTTP downloads

Two runtime defects found while getting an image workflow running.

smartbotic.log.debug was documented in docs/nodes.md but never bound: the
engine registered only info, warn and error. Any node calling it died with
"TypeError: not a function", which is what broke the RSS Reader node.

Binary downloads were corrupted end to end. smartbotic.http.request built
response.data with JS_NewString(body.c_str()), which truncates at the first
NUL byte and UTF-8 validates the rest. A JPEG begins FF D8 FF E0 00 ..., so
every image arrived as four U+FFFD replacement characters, and the HTTP
Request node then base64-encoded that instead of the file.

Adds response.dataBase64, built from the raw bytes in C++, and switches the
text path to JS_NewStringLen so a NUL no longer truncates it. The HTTP
Request node prefers dataBase64 for binary mode and derives size and
checksum from the real bytes rather than the mangled string.

Also exposes the resolved request URL on the result, so downstream nodes can
record where a file came from without re-deriving the interpolated value.
fszontagh il y a 1 mois
Parent
commit
18466138e6
2 fichiers modifiés avec 38 ajouts et 8 suppressions
  1. 15 6
      nodes/core/http-request.js
  2. 23 2
      src/runner/engine/script_engine.cpp

+ 15 - 6
nodes/core/http-request.js

@@ -100,6 +100,7 @@ const inputSchema = {
 const outputSchema = {
   type: 'object',
   properties: {
+    url: { type: 'string', description: 'Resolved request URL after interpolation' },
     statusCode: { type: 'number' },
     headers: { type: 'object' },
     body: { type: 'any' },
@@ -288,6 +289,7 @@ async function execute(config, input, context) {
     }
 
     const result = {
+      url: url,
       statusCode: response.status,
       headers: responseHeaders,
       ok: response.status >= 200 && response.status < 300
@@ -295,11 +297,18 @@ async function execute(config, input, context) {
 
     // Handle response based on mode
     if (responseMode === 'binary') {
-      // Get the response data - it's already a string from HTTP
+      // Prefer dataBase64: the runtime builds it from the raw bytes, whereas
+      // response.data is UTF-8 validated and truncated at the first NUL, which
+      // destroys images and every other binary payload.
+      const hasRawBase64 = typeof response.dataBase64 === 'string' && response.dataBase64.length > 0;
       const rawData = typeof response.data === 'string' ? response.data : JSON.stringify(response.data);
 
-      // Base64 encode for binary mode
-      const base64Data = smartbotic.utils.base64Encode(rawData);
+      const base64Data = hasRawBase64
+        ? response.dataBase64
+        : smartbotic.utils.base64Encode(rawData);
+      const byteLength = hasRawBase64
+        ? Math.floor(base64Data.replace(/=+$/, '').length * 3 / 4)
+        : rawData.length;
       const filename = extractFilename(responseHeaders, url);
       const mimeType = contentType.split(';')[0].trim() || 'application/octet-stream';
 
@@ -308,7 +317,7 @@ async function execute(config, input, context) {
         data: base64Data,
         mimeType: mimeType,
         filename: filename,
-        size: rawData.length
+        size: byteLength
       };
 
       result.file = fileObject;
@@ -320,12 +329,12 @@ async function execute(config, input, context) {
         const ttlHours = config.downloadTtlHours || 0;
         const ttlMs = ttlHours > 0 ? ttlHours * 60 * 60 * 1000 : 0;
 
-        const checksum = smartbotic.utils.sha256(rawData);
+        const checksum = smartbotic.utils.sha256(base64Data);
 
         const storageDoc = {
           filename: filename,
           mimeType: mimeType,
-          size: rawData.length,
+          size: byteLength,
           sourceUrl: url,
           checksum: checksum,
           downloadedAt: Date.now(),

+ 23 - 2
src/runner/engine/script_engine.cpp

@@ -1133,17 +1133,27 @@ static JSValue js_http_request(JSContext* ctx, JSValue this_val, int argc, JSVal
             content_type = resp_headers["content-type"].get<std::string>();
         }
 
+        // Binary-safe view of the body. "data" below goes through JS_NewString*, which
+        // UTF-8 validates and would replace every invalid byte, so it cannot carry
+        // images or other binary payloads. dataBase64 preserves the raw bytes.
+        JS_SetPropertyStr(ctx, result, "dataBase64",
+                          JS_NewString(ctx, base64Encode(response.body).c_str()));
+
         if (content_type.find("application/json") != std::string::npos && !response.body.empty()) {
             JSValue data = JS_ParseJSON(ctx, response.body.c_str(), response.body.size(), "<body>");
             if (JS_IsException(data)) {
                 // JSON parse failed, use as string
                 JS_FreeValue(ctx, JS_GetException(ctx));
-                JS_SetPropertyStr(ctx, result, "data", JS_NewString(ctx, response.body.c_str()));
+                JS_SetPropertyStr(ctx, result, "data",
+                                  JS_NewStringLen(ctx, response.body.data(), response.body.size()));
             } else {
                 JS_SetPropertyStr(ctx, result, "data", data);
             }
         } else {
-            JS_SetPropertyStr(ctx, result, "data", JS_NewString(ctx, response.body.c_str()));
+            // JS_NewStringLen rather than c_str(): a NUL byte in the body would
+            // otherwise truncate the string at that point.
+            JS_SetPropertyStr(ctx, result, "data",
+                              JS_NewStringLen(ctx, response.body.data(), response.body.size()));
         }
 
         return result;
@@ -1217,6 +1227,17 @@ void ScriptEngine::setupBuiltinAPIs() {
 
     // smartbotic.log
     JSValue log = JS_NewObject(ctx);
+    JS_SetPropertyStr(ctx, log, "debug", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
+        if (argc > 0) {
+            const char* str = JS_ToCString(ctx, argv[0]);
+            if (str) {
+                LOG_DEBUG("[JS] {}", str);
+                JS_FreeCString(ctx, str);
+            }
+        }
+        return JS_UNDEFINED;
+    }, "debug", 1));
+
     JS_SetPropertyStr(ctx, log, "info", JS_NewCFunction(ctx, [](JSContext* ctx, JSValue this_val, int argc, JSValue* argv) -> JSValue {
         if (argc > 0) {
             const char* str = JS_ToCString(ctx, argv[0]);