caddy fmt deletes an opening brace at the end of the input

Open Beginner friendly
#8,046 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
88/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
go
Domain
cli, tooling

Research direction

Start in caddyconfig/caddyfile/formatter.go around the brace handling referenced in the issue, then read TestFormatter for the existing formatter cases. Reproduce the examples with caddy fmt and verify the added cases preserve a trailing opening brace. Confirm the formatter remains idempotent and run the relevant Caddyfile tests.

Written by the indexing model from the issue text.

Description

Issue Details

caddyfile.Format silently deletes an opening brace when the brace is the last non-space character of the input, so caddy fmt removes a character the user typed.

input   "localhost {"   ->  output "localhost\n"
input   "{"             ->  output "\n"
input   "{ {"           ->  output "{\n"

Reproduce with the CLI:

printf 'localhost {' > Caddyfile
caddy fmt Caddyfile
localhost
{"level":"error","ts":...,"msg":"Caddyfile:1: Caddyfile input is not formatted; Tip: use '--overwrite' to update your Caddyfile in-place instead of previewing it. Consult '--help' for more options"}

The brace is gone from the preview, and the message then points the user at --overwrite, which writes that result back over their file.

This matters most with format-on-save, and with caddy fmt --overwrite the deletion is written back to the user's file. A Caddyfile whose last line has just opened a block is a normal intermediate state while editing, and formatting it should not drop the brace.

It also breaks idempotence, which this repo already checks for in caddyconfig/caddyfile/formatter_fuzz.go:

Format("{ {")  ->  "{\n"
Format("{\n")  ->  "\n"

The reason that fuzz target never reported it is that it only uses the idempotence result as a go-fuzz corpus priority (return 1 / return 0), so a non-idempotent input is deprioritised rather than reported as a failure.

Cause

Format writes { lazily. On case ch == '{' it records openBrace = true / openBraceWritten = false and continues, and the brace is only emitted later, when a following regular character is processed:

https://github.com/caddyserver/caddy/blob/master/caddyconfig/caddyfile/formatter.go#L355-L358

If the input ends while a brace is still pending, the loop exits and nothing flushes it, so the brace is never written. There is no flush after the loop; the function goes straight to bytes.TrimSpace and returns.

Proposed fix

Flush a pending brace after the read loop ends. I have this working locally as a four-line change, with two cases added to TestFormatter, and the full test suite passes. Happy to open a PR if you would like it.

Assistance Disclosure

AI used

If AI was used, describe the extent to which it was used.

I used Claude Code to fuzz the Caddyfile formatter for idempotence, which is how this was found, and to help trace the root cause and draft the fix and the test cases. I reviewed the change, confirmed the behaviour before and after it, and ran the test suite locally.

Dominant language
Go
Stars
75.9k
Forks
5k
Avg merge
3d 6h
Merged PRs (30d)
28

Contributor guide

Open the contributing guide

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.

More from caddyserver/caddy

All issues in caddyserver/caddy

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.