Skip to content

Commit b37f66e

Browse files
authored
Fix(cf curl): Prevent errors when handling binary API responses (#3605)
1 parent b25c8e5 commit b37f66e

File tree

2 files changed

+119
-3
lines changed

2 files changed

+119
-3
lines changed

command/v7/curl_command.go

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package v7
22

33
import (
4+
"bytes"
5+
"net/http"
46
"net/http/httputil"
57
"os"
8+
"strings"
69

710
"code.cloudfoundry.org/cli/command/flag"
811
"code.cloudfoundry.org/cli/command/translatableerror"
@@ -39,9 +42,9 @@ func (cmd CurlCommand) Execute(args []string) error {
3942
}
4043

4144
var bytesToWrite []byte
42-
43-
if cmd.IncludeResponseHeaders {
44-
headerBytes, _ := httputil.DumpResponse(httpResponse, false)
45+
var headerBytes []byte
46+
if cmd.IncludeResponseHeaders && httpResponse != nil {
47+
headerBytes, _ = httputil.DumpResponse(httpResponse, false)
4548
bytesToWrite = append(bytesToWrite, headerBytes...)
4649
}
4750

@@ -54,9 +57,38 @@ func (cmd CurlCommand) Execute(args []string) error {
5457
}
5558

5659
cmd.UI.DisplayOK()
60+
return nil
61+
}
62+
63+
// Check if the response contains binary data
64+
if isBinary(httpResponse, responseBodyBytes) {
65+
// For binary data, write response headers with string conversion
66+
// and the response body without string conversion
67+
if cmd.IncludeResponseHeaders {
68+
cmd.UI.DisplayTextLiteral(string(headerBytes))
69+
}
70+
cmd.UI.GetOut().Write(responseBodyBytes)
5771
} else {
5872
cmd.UI.DisplayText(string(bytesToWrite))
5973
}
6074

6175
return nil
6276
}
77+
78+
// isBinary determines if the provided `data` is likely binary content.
79+
// It first checks if the given `contentType` (e.g., from an HTTP header) is a known binary MIME type.
80+
// If not, it then scans the `data` byte slice for the presence of null bytes (0x00),
81+
// which are a strong heuristic for binary data.
82+
// Returns `true` if identified as binary, `false` otherwise.
83+
func isBinary(response *http.Response, data []byte) bool {
84+
responseContextType := ""
85+
if response != nil && response.Header != nil {
86+
responseContextType = response.Header.Get("Content-Type")
87+
}
88+
if strings.Contains(responseContextType, "image/") ||
89+
strings.Contains(responseContextType, "application/octet-stream") ||
90+
strings.Contains(responseContextType, "application/pdf") {
91+
return true
92+
}
93+
return bytes.ContainsRune(data, 0x00) // Check for null byte
94+
}

command/v7/curl_command_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,4 +149,88 @@ var _ = Describe("curl Command", func() {
149149
})
150150
})
151151

152+
When("the response contains binary data", func() {
153+
var binaryData []byte
154+
155+
BeforeEach(func() {
156+
// Create binary data with null bytes (like a droplet file)
157+
binaryData = []byte{0x50, 0x4B, 0x03, 0x04, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00}
158+
fakeActor.MakeCurlRequestReturns(binaryData, &http.Response{
159+
Header: http.Header{
160+
"Content-Type": []string{"application/octet-stream"},
161+
},
162+
}, nil)
163+
})
164+
165+
It("writes binary data directly to stdout without string conversion", func() {
166+
Expect(executeErr).NotTo(HaveOccurred())
167+
Expect(testUI.Out).To(Say(string(binaryData)))
168+
})
169+
170+
When("Content-Type is not a known binary MIME type", func() {
171+
BeforeEach(func() {
172+
// Create binary data with null bytes (like a droplet file)
173+
binaryData = []byte{0x50, 0x4B, 0x03, 0x04, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00}
174+
fakeActor.MakeCurlRequestReturns(binaryData, &http.Response{
175+
Header: http.Header{
176+
"Content-Type": []string{"text/plain"},
177+
},
178+
}, nil)
179+
})
180+
It("inspects the response data and writes binary data directly to stdout without string conversion", func() {
181+
Expect(executeErr).NotTo(HaveOccurred())
182+
Expect(testUI.Out).To(Say(string(binaryData)))
183+
})
184+
})
185+
186+
When("include-response-headers flag is set", func() {
187+
BeforeEach(func() {
188+
cmd.IncludeResponseHeaders = true
189+
})
190+
191+
It("writes headers as text and binary data separately", func() {
192+
Expect(executeErr).NotTo(HaveOccurred())
193+
194+
// Check that headers are written as text (using DisplayTextLiteral)
195+
Expect(testUI.Out).To(Say("Content-Type: application/octet-stream"))
196+
197+
// Check that binary data is preserved
198+
Expect(testUI.Out).To(Say(string(binaryData)))
199+
})
200+
})
201+
202+
When("output file is specified", func() {
203+
BeforeEach(func() {
204+
outputFile, err := os.CreateTemp("", "binary-output")
205+
Expect(err).NotTo(HaveOccurred())
206+
cmd.OutputFile = flag.Path(outputFile.Name())
207+
})
208+
209+
AfterEach(func() {
210+
os.RemoveAll(string(cmd.OutputFile))
211+
})
212+
213+
It("writes binary data to the file correctly", func() {
214+
Expect(executeErr).NotTo(HaveOccurred())
215+
216+
fileContents, err := os.ReadFile(string(cmd.OutputFile))
217+
Expect(err).ToNot(HaveOccurred())
218+
Expect(fileContents).To(Equal(binaryData))
219+
})
220+
})
221+
})
222+
223+
When("the response is empty", func() {
224+
BeforeEach(func() {
225+
fakeActor.MakeCurlRequestReturns([]byte{}, &http.Response{
226+
Header: http.Header{},
227+
}, nil)
228+
})
229+
230+
It("handles empty response correctly", func() {
231+
Expect(executeErr).NotTo(HaveOccurred())
232+
Expect(testUI.Out).To(Say(""))
233+
})
234+
})
235+
152236
})

0 commit comments

Comments
 (0)