bug: `curl NAME` exports an unquoted command that fails before the request is sent

Open Beginner friendly
#49 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
82/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
bash, python, shell
Domain
cli

Research direction

Start in cli.py at lines 453–459, where parts_curl is assembled and printed; the issue identifies the unquoted join as the failure point. Read the existing shlex usage and run the relevant CLI tests, then add a regression test for a request with a space-bearing header and JSON body. Done when the exported command preserves each argument and the test confirms the request is sent with the expected header and body.

Written by the indexing model from the issue text.

Description

bug

Summary

curl NAME prints a command that a shell cannot run. Every argument is emitted
raw, with no quoting, so any header value or body containing a space is split
into multiple arguments by the shell, the URL is no longer the last argument,
and curl exits non-zero without sending the request at all. The REPL reports
nothing wrong: it printed an export, printed no error, and the session continues
normally. The user pastes the output into a terminal and gets a wall of curl
errors instead of the request.

parts_curl is built correctly as a list of argv tokens at cli.py:453-458;
the bug is the join at cli.py:459, which pastes the tokens together without
shlex.quote().

Steps to reproduce

Save any request that has a JSON body, then export it:

mkdir -p /tmp/gapi-demo/.git-api/requests && cd /tmp/gapi-demo
git init -q
cat > .git-api/requests/mk.json <<'JSON'
{
  "name": "mk",
  "request": {
    "method": "POST",
    "url": "http://127.0.0.1:8000/users",
    "headers": {"Content-Type": "application/json"},
    "body": "{\"name\": \"Yunare Maia\", \"role\": \"dev\"}"
  },
  "response": {"status": 200, "headers": {}, "body": "{\"ok\":true}"}
}
JSON
git-api

Inside the REPL:

curl mk

Actual vs Expected

Actual — REPL stdout, verbatim:

default> curl \
   -X \
   POST \
   -H \
   Content-Type: application/json \
   -d \
   {"name": "Yunare Maia", "role": "dev"}} \
   http://127.0.0.1:8000/users
default>

The REPL exits 0. It never reports a problem.

Pasting that exact text into bash (copied byte-for-byte out of the REPL's own
stdout, no re-formatting):

=== bash <exported> ===
exit: 3
stdout: (empty)
stderr: curl: (6) Could not resolve host: application
curl: (3) URL rejected: Malformed input to a URL function
curl: (3) URL rejected: Port number was not a decimal number between 0 and 65535
curl: (3) unmatched close brace/bracket in position 4:
dev}
   ^

A local echo server listening on the target port recorded zero requests:

[]

Expected body was {"name": "Yunare Maia", "role": "dev"}. Nothing was sent.

Root cause

cli.py:453-459:

parts_curl = ["curl", "-X", req["method"]]
for k, v in req.get("headers", {}).items():
    parts_curl += ["-H", f"{k}: {v}"]
if req.get("body"):
    parts_curl += ["-d", req["body"]]
parts_curl.append(req["url"])
print(" \\\n   ".join(parts_curl))

The shell does not re-split on newlines preceded by \ the way it splits on
spaces. What does split -H and -d values is the space inside them:
Content-Type: application/json becomes two argv entries, and the body becomes
eleven. The URL lands somewhere in the middle of the argv, where curl tries to
read dev} as a URL.

Any header with a space triggers this, which includes the single most common
header in the tool's own documented use case — Authorization: Bearer <token>:

default> curl \
   -X \
   GET \
   -H \
   Authorization: Bearer *** \
   http://127.0.0.1:9/users

That exports as four argv entries (Authorization:, Bearer, ***) instead of
one. This is the default shape of every authenticated request the REPL can send,
so the exporter is broken for essentially every real use.

Note the asymmetry: save sanitizes names, replay interpolates and warns
about unresolved variables, and execute_request catches URLError. curl is
the one command in the file that hands user data to another program to
re-interpret, and it is the one with no escaping at all.

Suggested fix

shlex.quote() each argument, so the shell sees exactly the argv the REPL
built. The module already imports shlex for the input side, so this needs no
new dependency:

parts_curl = ["curl", "-X", req["method"]]
for k, v in req.get("headers", {}).items():
    parts_curl += ["-H", f"{k}: {v}"]
if req.get("body"):
    parts_curl += ["-d", req["body"]]
parts_curl.append(req["url"])
print(" \\\n   ".join(shlex.quote(a) for a in parts_curl))

That makes the export round-trip: whatever the request JSON holds comes back out
as one argv entry per token.

Two things worth deciding alongside it:

  1. Test the round-trip. The regression guard that matters is: save a request
    with a space-bearing header and a JSON body, run curl NAME, feed the output
    to bash, and assert the server received the exact body and the exact
    Authorization header. That catches both the quoting bug and any future
    re-introduction. tests/test_install_name.py already establishes the pattern
    of pinning a behaviour that a real consumer depends on.
  2. --data-binary for JSON. -d strips newlines from a @-prefixed file
    argument and does form-decoding hints on some curl builds; --data-binary
    is the accurate choice for a verbatim body. Secondary to the quoting fix,
    but it belongs in the same change since both are in the same three lines.

Environment

git-api-py 0.1.0, commit 1a2e49b
python 3.14.7, venv install via `pip install -e '.[dev]'`
repro: repro/probe_round3.py (drives the installed console script, then bash)
Dominant language
Python
Stars
0
Forks
1
PR merge metrics
No merged PRs in 30d

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.