{"record":{"id":"db0d4134a18d77df","repo":"caddyserver/caddy","slug":"incomplete-heredoc-s-on-line-d-expected-endi","errorCode":null,"errorMessage":"incomplete heredoc <<%s on line #%d, expected ending marker %s","messagePattern":"incomplete heredoc <<(.+?) on line #(.+?), expected ending marker (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"caddyconfig/caddyfile/lexer.go","lineNumber":128,"sourceCode":"\tvar heredocMarker string\n\n\tmakeToken := func(quoted rune) bool {\n\t\tl.token.Text = string(val)\n\t\tl.token.wasQuoted = quoted\n\t\tl.token.heredocMarker = heredocMarker\n\t\treturn true\n\t}\n\n\tfor {\n\t\t// Read a character in; if err then if we had\n\t\t// read some characters, make a token. If we\n\t\t// reached EOF, then no more tokens to read.\n\t\t// If no EOF, then we had a problem.\n\t\tch, _, err := l.reader.ReadRune()\n\t\tif err != nil {\n\t\t\tif len(val) > 0 {\n\t\t\t\tif inHeredoc {\n\t\t\t\t\treturn false, fmt.Errorf(\"incomplete heredoc <<%s on line #%d, expected ending marker %s\", heredocMarker, l.line+l.skippedLines, heredocMarker)\n\t\t\t\t}\n\n\t\t\t\treturn makeToken(0), nil\n\t\t\t}\n\t\t\tif err == io.EOF {\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t\treturn false, err\n\t\t}\n\n\t\t// detect whether we have the start of a heredoc\n\t\tif (!quoted && !btQuoted) && (!inHeredoc && !heredocEscaped) &&\n\t\t\tlen(val) > 1 && string(val[:2]) == \"<<\" {\n\t\t\t// a space means it's just a regular token and not a heredoc\n\t\t\tif ch == ' ' {\n\t\t\t\treturn makeToken(0), nil\n\t\t\t}\n","sourceCodeStart":110,"sourceCodeEnd":146,"githubUrl":"https://github.com/caddyserver/caddy/blob/50e54ee279aa1e504fe218ca49ab6ae16c100410/caddyconfig/caddyfile/lexer.go#L110-L146","documentation":"While lexing a heredoc (<<END ... END), the lexer reached end-of-input before seeing the closing marker line. The message names the marker and the line where the heredoc started, so you can find the unterminated block.","triggerScenarios":"A Caddyfile contains <<MARKER but no subsequent line equal to MARKER before EOF. This can be a missing closing marker, a marker with trailing characters/spaces, or CR/LF mismatches making the closing line not match exactly.","commonSituations":"Copy-pasting heredoc config (e.g. inline HTML or TLS policies) and dropping the last line; using an editor that trims trailing whitespace on the closing marker; Windows line endings where the marker line has trailing '\\r'.","solutions":["Add the closing marker on its own line, exactly matching the opening marker, no leading/trailing whitespace","Check for trailing spaces or CR characters on the closing marker line (cat -A)","Confirm the marker only uses alphanumerics, dashes, underscores","Ensure the file ends with a newline after the marker"],"exampleFix":"# before\nrespond <<HTML\n  <p>hi</p>\n# (EOF, no closing marker)\n# after\nrespond <<HTML\n  <p>hi</p>\nHTML","handlingStrategy":"validation","validationCode":"// before adapt: every '<<MARKER' opener must have a line equal to MARKER\ntype validator struct{ marker string; closed bool }\nfunc checkHeredocs(lines []string) error {\n    var open string\n    for _, ln := range lines {\n        t := strings.TrimSpace(ln)\n        if open == \"\" {\n            if strings.HasPrefix(t, \"<<\") { open = strings.TrimPrefix(t, \"<<\") }\n        } else if t == open {\n            open = \"\"\n        }\n    }\n    if open != \"\" { return fmt.Errorf(\"heredoc %s never closed\", open) }\n    return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always close heredocs with the bare marker at the desired indentation","Disable trailing-whitespace trimming for Caddyfiles in your editor","Prefer quoted respond bodies over heredocs for short strings"],"tags":["caddyfile","heredoc","lexer"],"backgroundTag":null,"analyzedSha":"50e54ee279aa1e504fe218ca49ab6ae16c100410","analyzedAt":"2026-08-15T09:20:21.641Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}