fix(stream): ensure transfer-encoding: chunked does not mess output
+ 58
- 21
@@ -0,0 +1,14 @@
+stages:
+  - name: build
+    image: alpine:3.18
+    workdir: .:/opt/app/
+    commands:
+      - git clone https://gitfoss.dev/ethicdevs/crystal-docker-api.git
+      - cd crystal-docker-api/
+      - shards install
+      - shards build
+    cached:
+      - ./lib
+      - ./shard.lock
+    # artifacts:
+    #  - ./bin/crystal-docker-api

src/docker/api/api_client.cr
@@ -100,28 +100,8 @@ class Docker::Api::ApiClient
         response
       end
 
-      # cleanup response body send over docker socket
-      raw = res_str
-        .gsub("10f1\r\n", "") # ?
-        .gsub("10f", "")      # ?
-        .gsub("11\r\n", "")   # ?
-        .gsub("11c4\r\n", "") # ?
-        .gsub("0\r\n", "")    # ?
-        .gsub("1321\r\n", "") # ?
-        .gsub("1322\r\n", "") # ?
-        .gsub("13\r\n", "")   # ?
-        .gsub("14\r\n", "")   # ?
-        .gsub("1d\r\n", "")   # ?
-        .gsub("4f\r\n", "")   # ?
-        .gsub("3a\r\n", "")   # ?
-        .gsub("3e\r\n", "")   # ?
-        .gsub("5e\r\n", "")   # ?
-
-      # pp path
-      # pp raw
-      # puts "\n"
-
       # Parse status line, headers and body
+      raw = res_str
       head, body = raw.split("\r\n\r\n", 2)
       body = body.chomp("\r\n").chomp("\r\n")
       status_line, *header_lines = head.split("\r\n")

...
@@ -140,6 +120,49 @@ class Docker::Api::ApiClient
         headers["Content-Length"] = "0"
       end
 
+      # Handle chunked response (Transfer-Encoding: chunked)
+      # (i.e. docker logs stream response)
+      if headers["Transfer-Encoding"]? && headers["Transfer-Encoding"] == "chunked"
+        json_payloads = 0
+        sb = [] of String
+        sb_chunk = [] of String
+        body.lines.each do |line|
+          begin
+            if line == "\0" || line == "0\r\n" || line == "0\n" # NUL-terminator
+              next
+            elsif (line.starts_with?('{') || line.starts_with?('[')) == false # size headers (json parse them...)
+              if headers["Content-Type"] == "application/vnd.docker.raw-stream"
+                sb_chunk << line
+              end
+              next
+            end
+            res = JSON.parse(line)
+            json_payloads += 1
+            sb << line
+          rescue
+            # this prevent header size to slip in result body
+            next
+          end
+        end
+
+        body = sb.join("\n")
+
+        if json_payloads > 1 && headers["Content-Type"] == "application/json"
+          begin
+            body = JSON.parse("["+sb.join(",\n")+"]").to_json # so json streaming works
+          rescue
+            body = sb.join("\n")
+          end
+        elsif headers["Content-Type"] == "application/vnd.docker.raw-stream"
+          body = sb_chunk[1..-2].join("\n").chomp("\n")
+        end
+      end
+
+      # puts "\n"
+      # pp path
+      # # pp headers
+      # pp body
+
       HTTP::Client::Response.new(
         status_code,
         status_code != 204 ? body : nil,