bug: `curl NAME` exports an unquoted command that fails before the request is sent
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 82/100
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
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:
- Test the round-trip. The regression guard that matters is: save a request
with a space-bearing header and a JSON body, runcurl NAME, feed the output
tobash, and assert the server received the exact body and the exact
Authorizationheader. That catches both the quoting bug and any future
re-introduction.tests/test_install_name.pyalready establishes the pattern
of pinning a behaviour that a real consumer depends on. --data-binaryfor JSON.-dstrips 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
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
pytest-dev/pyfakefs#1367 · 1 comment ·
Maintainers usually reply within 2 days
-
IFEvalG exclude_word_harder misses the keyword next to punctuation, at the start or end of the text, and in other letter casePossibly taken @damienen claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
allenai/open-instruct#2151 ·
Maintainers usually reply within 1 day
-
feat(plan): support convertorLabels and convertorAffinity in Plan resourcePossibly taken @AmenB claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
RedHatQE/openshift-python-wrapper#2849 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
Maintainers usually reply within 25 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
libnativeapi/nativeapi#21 ·