The final delimiter (the "encapsulation boundary") following the last multipart, is the same as for a body parts plus two hyphens.
Thus, the final delimiter looks as follows:
--<boundary>--
and not
--<boundary>--<boundary>--
as in your implementation ;)
A CRLF following the last delimiter won't hurt, also.
Edit:
A more extensive answer
The corresponding specification of a multipart/form-data
can be read in Returning Values from Forms: multipart/form-data RFC 7578 and in Multipurpose Internet Mail Extensions (MIME) Part Two: Media Types in Section 5.1 Multipart Media Type RFC 2388.
The multipart
media type is defined more precisely in RFC 2046 (which obsoletes RFC 1521, RFC 1522, RFC 1590).
The following are relevant excerpts from RFC 2388 and RFC 2046:
According RFC 2388, a multipart/form-data
contains a series of parts. Each part must contain a particular
content-disposition header (see RFC 2183) where the disposition type
is "form-data" and where the disposition has an additional parameter of "name". For example:
Content-Disposition: form-data; name="user"
Note: The most recent RFC for content-disposition header is defined in RFC 2231
(Updates: 2045, 2047, 2183; Obsoletes RFC 2184)).
The media-type multipart/form-data
strictly follows the rules of all multipart MIME data streams:
Subtypes of the "multipart" type must use an identical syntax. Subtypes may differ in their semantics,
and may impose additional restrictions on syntax, but must conform to the required syntax for
the "multipart" type.
Basically, the body of a multipart must contain one or more body parts, each preceded by a boundary delimiter line,
and the last one followed by a closing boundary delimiter line.
Each body part consists of a header area, a blank line, and a body area. The header area
is allowed to be empty.
Note: a body part is an entity, and not a RFC 882 message. That is, no headers are actually
required in body parts. The absence of a Content-Type header usually indicates that the
corresponding body has a content-type of "text/plain; charset=US-ASCII".
If the contents of a file is to be transfered, then "Content-Type" shall be set to the file's media type,
if known, otherwise "application/octet-stream".
Hint: The only header fields that have defined meaning for body parts are those whose names
begin with "Content-". All other header fields may be ignored in body parts.
Note: If multiple files are to be returned as the result of a single form entry, they
should be represented as a multipart/mixed
part embedded within the multipart/form-data
.
The original local file name may be supplied as well, either as a "filename" parameter either
of the "content-disposition: form-data" header or, in the case of multiple files, in a
"content-disposition: file" header of the subpart.
Common Syntax
The common syntax for the multipart
media type is defined in RFC 2046 § 5.1.1
Here is a simplified more comprehensive form:
A multipart must have a Content-Type. For example:
Content-Type: multipart/subtype; boundary=gc0p4Jq0M2Yt08j34c0p
For the boundary there are certain limitations (please see RFC 2046). In practice, enclosing it in double quotes makes it more robust:
Content-Type: multipart/subtype; boundary="---- boundary which requires quotes -----"
Each part is preceded by a boundary delimiter. The boundary delimiter MUST occur at the
beginning of a line (that is, following a CRLF). Conceptually, the CRLF belongs to the
boundary, rather to the preceding element:
boundary-delimiter := CRLF "--" boundary
Note: the boundary may be followed by zero or more whitespace, which is not shown in
this BNF.
A multipart body consists of one or more encapsulations followed by a closing delimiter:
multipart-body := +encapsulation
end-boundary-delimiter
where an encapsulation is a boundary delimiter followed by a CRLF followed by the body part:
encapsulation := boundary-delimiter CRLF
body-part
The body part (the entity) consists of entity-headers and the body:
body-part := MIME-part-headers [CRLF *OCTET]
Note: An entity-header (a MIME-part-header) will be delimited by a CRLF - as any other header.
The last part is followed by a closing delimiter:
end-boundary-delimiter := CRLF "--" boundary "--"
Caution:
Despite the massive amount of papers, the above definition for boundaries is still unclear - if not ambiguous!
In RFC 2046, § 5.1.1 Common Syntax, it states:
It (the boundary) is then terminated by either another CRLF and
the header fields for the next part, or by two CRLFs, in which case
there are no header fields for the next part.
and
The CRLF preceding the boundary delimiter line is conceptually
attached to the boundary...
A body-part may be completely empty (according the BNF). A boundary will be
followed by one CRLF and then followed by either a boundary-delimiter or a end-boundary-delimiter. Only since the next delimiter starts itself with a CRLF, the preceding boundary has two subsequent CRLFs!
Examples
Note, all CR and LF are shown explicitly.
Example: multipart-body with one part, two entity headers:
boundary = "1234567890"
\r\n--1234567890\r\n
header1: value1\r\n
header2: value2\r\n
\r\n<data>
\r\n--1234567890--
Example: multipart-body with two parts:
\r\n--1234567890\r\n
header1: value1\r\n
header2: value2\r\n
\r\n<data1>
\r\n--1234567890\r\n
header1: value1\r\n
\r\n<data2>
\r\n--1234567890--
Example: multipart-body with no headers:
\r\n--1234567890\r\n
\r\n<data1>
\r\n--1234567890--
Example: multipart-body with empty body-part:
\r\n--1234567890\r\n
\r\n--1234567890--