Close the header section when a part has an empty body - #410
Merged
jhillyerd merged 2 commits intoAug 12, 2026
Conversation
Part.Encode only wrote the empty line separating the header section from the body when the part had content, so a message built with no body ended after its last header line. Reading that back with net/textproto's ReadMIMEHeader returns the headers along with io.EOF, because the header section was terminated by end of input rather than by the empty line. Write the separator for parts without children even when the body is empty. Parts with children are unchanged: they already get that line from the leading CRLF of the first boundary marker. Fixes jhillyerd#196
hdimer
marked this pull request as ready for review
August 12, 2026 03:30
testifylint's require-error rule: an error assertion that gates later assertions should stop the test (require) rather than continue (assert).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #196.
The problem
Part.Encodewrites the empty line that separates the header section from the body only when the part has content:So a message with an empty body ends right after its last header line. Reproducing the issue report with current
main:The concrete consequence: the header section is terminated by end of input rather than by the empty line, so
net/textproto'sReadMIMEHeaderreturns the headers along withio.EOF, and a consumer that checks the error rejects the message.To be precise about the spec: RFC 5322's ABNF is
message = fields [CRLF body]and RFC 2046's isbody-part := MIME-part-headers [CRLF *OCTET], so a header-only message is grammatical and this is not a MUST violation. It is an interop and clarity problem: without the empty line a reader cannot distinguish "header section finished" from "input truncated mid-header", which is exactly whatReadMIMEHeaderreports.The change
Write the separator for parts with no children even when the body is empty. Parts with children are untouched, because the leading CRLF of the first boundary marker already supplies that line.
Behaviour changes
Four golden files gain the two missing bytes. One thing worth calling out explicitly:
testdata/encode/part-empty.goldengoes from 0 bytes to a bare CRLF. That is&enmime.Part{}, with no headers at all. I deliberately did not special-case it: guarding onlen(p.Header) > 0has no basis in the spec, and it would reintroduce the bug exactly where it bites hardest, since a header-less part used as a multipart child would still emit nothing at all between delimiters."\r\n"is the honest encoding of "empty header section, empty body". Happy to add the guard if you would rather keep that file empty.build-qp-addr-headers.goldenis the issue's own scenario:Builder()with no.Text()/.HTML().part-header-only.goldenandpart-header-only-default-encoding.goldenare leaf parts with headers and no body.I re-encoded the whole
testdatacorpus with and without the patch: 15 files change, every one by exactly +2 bytes on a genuinely empty body, and nothing else moves. Parse → encode → parse → encode reaches the same fixed point as before, so the change adds no round-trip instability.Tests
TestEncodeEmptyBodyReadableByTextprotoreproduces the issue's scenario end to end and assertsReadMIMEHeaderno longer reports EOF.TestEncodeEmptyChildEndsHeaderspins the multipart case, which no golden covered: it would fail if the fix emitted a stray blank line in the parent, so it guards against an over-broad version of this patch.TestEncodePartForcedCTEEmptyContenttable, which already builds the nil-content and zero-byte-ContentReaderfixtures but only checked headers.All three fail on
mainand pass with the change.make test(go test -race ./...) andmake lintare clean.Disclosure: I used an AI assistant while investigating and writing this patch. I reproduced the bug, verified the corpus differential and the
ReadMIMEHeaderbehaviour myself, and I stand behind the change.Used AI assistance on this; I reviewed and tested the change myself.