Desk Clarity

Everyday document & browser help

Curl Data From a File Loses Newlines: Verify --data-binary by Bytes

By ON Lab · Sources checked November 8, 2026

With curl, --data @file does not preserve a file’s body bytes unchanged: its file-input handling strips carriage returns and newlines. Use --data-binary @file when the request body must retain those bytes. Verify this against a harmless local receiver before changing a real API request, and keep the content type as a separate decision.

Why identical-looking inputs differ

A quoted command-line string and @filename are two different input routes. The historical curl report we inspected observed a content-length difference when sending formatted JSON from a file. The maintainer pointed to the documented file-input behavior. Adding Content-Type: application/json does not change how curl reads the file.

For ordinary JSON, removing whitespace between tokens may leave equivalent parsed data. That does not establish byte equality. Other text protocols, line-delimited formats and byte-dependent verification can require the original separators. First identify whether your requirement is “same parsed value” or “same exact body.”

A controlled byte comparison

Our fixture used a 14-byte synthetic file: five letters, CRLF, six letters and a final LF.

first\r\nsecond\n

A loopback-only HTTP receiver read the length declared by curl and returned the received bytes. With curl 8.7.1, we observed:

Input modeReceived representationLength
--data @body.txtb'firstsecond'11 bytes
--data-binary @body.txtb'first\r\nsecond\n'14 bytes

No external endpoint, credentials or real message was used. Here is a local receiver you can run in a disposable test directory. It accepts exactly two requests and then exits.

from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        body = self.rfile.read(int(self.headers["Content-Length"]))
        print(repr(body), len(body), flush=True)
        self.send_response(200)
        self.end_headers()
    def log_message(self, *args):
        pass
with HTTPServer(("127.0.0.1", 8766), Handler) as server:
    for _ in range(2):
        server.handle_request()

In a second terminal, write the fixture using Python so the intended line endings are explicit, then issue the two local requests. Use another unused local port in both places if 8766 is occupied.

python3 -c 'from pathlib import Path; Path("body.txt").write_bytes(b"first\r\nsecond\n")'
curl --noproxy '*' --data @body.txt http://127.0.0.1:8766/
curl --noproxy '*' --data-binary @body.txt http://127.0.0.1:8766/

Check the receiver’s byte representation and length. The commands use a literal loopback URL and bypass proxies for this local test. Do not replace it with a production URL merely to compare formatting: both options send a POST body.

Content type and HTTP status remain separate

The body-preserving option does not automatically choose your API’s media type. For a JSON API, set the documented JSON content type separately after the local comparison. For a line-based text protocol, use that protocol’s actual documented type. Multipart uploads and form encoding are different contracts; a byte-preserving option is not a substitute for them.

Likewise, a successful curl process is not proof that a server accepted the request’s business action. Use the HTTP-status and curl-exit check when diagnosing a rejected response. Do not repeat a consequential request until you know whether the earlier one completed.

If bytes still differ

  1. Read the original file as bytes and record its size. An editor may already have changed line endings.
  2. Compare the local receiver output with that file, not only with a pretty-printed display.
  3. Check whether multiple data options concatenate bodies or whether a wrapper changed the arguments.
  4. If local input is exact but the remote application reports a different value, inspect its documented decoding and parsing stages.

The standard curl tool already supports this comparison; no paid HTTP client is necessary. The added value here is a falsifiable byte-level fixture and a separation of payload preservation, media type, transport status and application outcome.

Method: curl’s current official manual, a historical curl issue, and an original macOS curl 8.7.1 loopback fixture. The receiver was closed after both tests. No external API behavior, signatures, proxy transformations or production request was tested.

Sources