net/mime
The media type of a file, the Content-Type header, and multipart form bodies.
import net.mime
A media type (MIME) tells the browser and other programs what the content is, such as image/png. This package finds the type from a file extension and reads the Content-Type header.
Forms that upload a file send a multipart/form-data body: several parts with a boundary between them. ParseMultipart reads one and MultipartWriter builds one.
The package also has StatusText, the standard reason phrase of an HTTP status code. It is used by net/http, which is where you will usually meet these functions.
What trips people up
- The type table is short.
MimeExtknows about 25 extensions:html htm css js mjs json map png jpg jpeg gif svg ico webp txt md pdf xml wasm zip mp4 webm mp3 wav woff woff2. Everything else,csv,avif,ttfanddocxincluded, becomesapplication/octet-stream, so handle the extra types your site serves before calling it. - Lookups and comparisons are case sensitive.
MimeTypedoes not lower the extension,ParseContentTypekeeps the case of the media type and the parameter names, andParamGetmatches the name exactly, soCharset=UTF-8is not found by"charset". Lower both sides before comparing. - Only three types get a charset:
text/html,text/plainandtext/markdowncome withcharset=utf-8; CSS and JavaScript do not. - Binary uploads need the bytes.
ParseMultipartworks on raw memory and keeps each body in aByteBuf, but if the request body has passed through astron the way, it is already cut at the first zero byte.
The guide below shows the places a web program meets media types: serving files with the right Content-Type, deciding how to read a request body, and handling file uploads on both sides.
The right Content-Type for a file
A browser decides what to do with a response from its Content-Type, not from the file name, so a stylesheet served as application/octet-stream is ignored. MimeType maps a file name to its type by extension, with charset=utf-8 added to HTML, plain text and Markdown, and falls back to application/octet-stream for names it does not know. MimeExt does the same for a bare extension without the dot, such as png, when you have no file name.
import net.mime
func header_for(path: str): str:
ret "Content-Type: " + mime.MimeType(path.lower())
end
func main:
files := ["index.html", "app.js", "style.css", "logo.svg", "photo.JPG", "fonts/vazir.woff2", "report.pdf", "data.bin", "README"]
each f in files:
println f, "->", header_for(f)
end
println mime.MimeType("photo.JPG"), "(without lower)"
endThe lookup is case sensitive: photo.JPG is only recognised after lower(). Files from cameras and Windows often have upper case extensions, so lower the name before the lookup, as header_for does.
Reading the Content-Type of a request
A server has to look at the Content-Type header before it reads the body: JSON, a URL encoded form and a multipart upload are all read differently. ParseContentType splits the header into media_type and its parameters, and ParamGet returns one parameter, such as charset or boundary, with any quotes removed, or an empty string when it is absent.
import net.mime
func describe(header: str): str:
ct := mime.ParseContentType(header)
media := ct.media_type.lower()
mut charset := mime.ParamGet(ct, "charset")
if charset.len() == 0: charset = "none" end
if media == "application/json":
ret "JSON body, charset " + charset
else media == "application/x-www-form-urlencoded":
ret "URL encoded form"
else media == "multipart/form-data":
ret "upload, boundary " + mime.ParamGet(ct, "boundary")
else media.find("text/") == 0:
ret "plain text, charset " + charset
end
ret "unknown: [" + media + "]"
end
func main:
println describe("application/json")
println describe("application/json; charset=utf-8")
println describe("application/x-www-form-urlencoded")
println describe("multipart/form-data; boundary=\"abc123\"")
println describe("text/csv;charset=windows-1256")
println describe("Application/JSON")
println describe("")
endMedia types are case insensitive by the standard, but media_type keeps the case it was sent in. Compare ct.media_type.lower(), otherwise a client that sends Application/JSON falls through to the unknown branch.
Building and reading a file upload
An HTML form with a file field sends multipart/form-data: each field becomes a part with its own headers, separated by a boundary string. NewMultipartWriter starts one, WriteField adds a text field, WriteFileStr and WriteFileBytes add a file, and CloseWriter returns the finished body. ContentTypeHeader gives the matching header with the boundary in it. On the receiving side, ParseMultipart takes the body and the boundary and returns the parts; a part with a non-empty filename is a file.
import net.mime
import compress
func handle_upload(content_type: str, body: compress.ByteBuf):
ct := mime.ParseContentType(content_type)
if ct.media_type != "multipart/form-data":
println "415 Unsupported Media Type"
ret
end
boundary := mime.ParamGet(ct, "boundary")
parts := mime.ParseMultipart(body.data, body.len, boundary)
each p in parts.items:
if p.filename.len() > 0:
println "file", p.name, p.filename, p.content_type, p.body.len, "bytes"
else:
println "field", p.name, "=", compress.BufToStrUnsafe(p.body)
end
end
end
func main:
mut form := mime.NewMultipartWriter()
mime.WriteField(form, "title", "Trip to Shiraz")
mime.WriteField(form, "album", "2026")
mime.WriteFileStr(form, "notes", "notes.txt", "", "Day 1: Eram garden")
photo := compress.BufFromStr("not really a jpeg")
mime.WriteFileBytes(form, "photo", "eram.jpg", "", photo.data, photo.len)
body := mime.CloseWriter(form)
handle_upload(mime.ContentTypeHeader(form), body)
handle_upload("application/json", body)
endWhen you pass an empty content type for a file part, the writer fills it in from the file name with MimeType, which is why notes.txt and eram.jpg got their types. A body that is not multipart/form-data should be refused with status 415 before any parsing.
An upload endpoint with net/http
The same two halves work over a real connection. The program below starts a net/http server on 127.0.0.1 with a router, posts a form with a text file to it and prints what the handler found. In the handler, Header gives the request's Content-Type and Body the raw body.
import net.http
import net.router
import net.mime
import compress
func upload(ctx: i64):
ct := mime.ParseContentType(http.Header(ctx, "Content-Type"))
body := http.Body(ctx)
parts := mime.ParseMultipart(body as void*, body.len(), mime.ParamGet(ct, "boundary"))
mut reply := ""
each p in parts.items:
reply += p.name + ": " + compress.BufToStrUnsafe(p.body) + "\n"
end
http.Text(ctx, reply)
end
func main:
mut r := router.NewRouter()
router.Post(r, "/upload", upload)
mut s := http.NewServer(18577)
http.SetHost(s, "127.0.0.1")
http.Use(s, r)
if not http.RunBackground(s):
println "port 18577 is busy"
ret 1
end
mut form := mime.NewMultipartWriter()
mime.WriteField(form, "user", "maryam")
mime.WriteFileStr(form, "todo", "todo.txt", "text/plain", "buy bread")
body := mime.CloseWriter(form)
headers := HashMap {} as HashMap<str, str>
headers.put("Content-Type", mime.ContentTypeHeader(form))
resp := http.PostWith("http://127.0.0.1:18577/upload", compress.BufToStrUnsafe(body), headers)
println resp.status
print resp.body
ret 0
endTwo limits to know. Body is a str, and a str ends at the first zero byte, so binary files such as images arrive cut short through this path; text uploads are fine. And ParseMultipart matches part headers such as Content-Disposition with their exact case, which is what browsers and this package's writer send.
Types
ContentTypeParam
struct ContentTypeParam| Name | Type | Description |
|---|---|---|
key | str = "" | The name, in the case it was sent. |
value | str = "" | The value. |
import net.mime
func main:
println mime.MimeExt("png"), mime.MimeExt("json"), mime.MimeExt(".png")
println mime.MimeType("/site/index.html"), mime.MimeType("style.css")
println mime.StatusText(200), mime.StatusText(404), mime.StatusText(503)
ct := mime.ParseContentType("text/html; charset=UTF-8; q=0.9")
println ct.media_type, ct.params.items.len(), mime.ParamGet(ct, "charset"), ct.params.items[1].key, ct.params.items[1].value
endContentTypeParams
struct ContentTypeParams| Name | Type | Description |
|---|---|---|
items | Vector | The parameters. |
The example for this item is shown with the ones above: net-mime-Types.salam
ContentType
struct ContentTypeContent-Type header.| Name | Type | Description |
|---|---|---|
media_type | str = "" | The media type, trimmed but in the case it was sent. |
params | ContentTypeParams | The parameters, such as charset. |
The example for this item is shown with the ones above: net-mime-Types.salam
MultipartHeader
struct MultipartHeader| Name | Type | Description |
|---|---|---|
key | str = "" | The name. |
value | str = "" | The value. |
import net.mime
import compress
import str
func main:
mut w := mime.NewMultipartWriter()
println str.StartsWith(mime.ContentTypeHeader(w), "multipart/form-data; boundary=")
mime.WriteField(w, "name", "Maryam")
mime.WriteFileStr(w, "note", "note.txt", "text/plain", "hello")
data := compress.BufFromStr("PNG")
mime.WriteFileBytes(w, "pic", "a.png", "image/png", data.data, data.len)
body := mime.CloseWriter(w)
println w.closed, body.len > 0
mut parts := mime.ParseMultipart(body.data, body.len, w.boundary)
each p in parts.items:
println p.name, "[" + p.filename + "]", "[" + p.content_type + "]", compress.BufToStrUnsafe(p.body), p.headers.items.len()
end
println parts.items[1].headers.items[0].key
println mime.PartIndex(parts, "note"), mime.PartIndex(parts, "missing")
println mime.PartValue(parts, "name"), "[" + mime.PartValue(parts, "missing") + "]"
mime.FreeMultipart(parts)
endMultipartHeaders
struct MultipartHeaders| Name | Type | Description |
|---|---|---|
items | Vector | The headers. |
The example for this item is shown with the ones above: net-mime-Multipart.salam
MultipartPart
struct MultipartPart| Name | Type | Description |
|---|---|---|
name | str = "" | The name of the form field. |
filename | str = "" | The file name, when the part is a file. |
content_type | str = "" | The content type of the part. |
headers | MultipartHeaders | Every header of the part. |
body | compress.ByteBuf | The contents of the part. |
The example for this item is shown with the ones above: net-mime-Multipart.salam
MultipartParts
struct MultipartParts| Name | Type | Description |
|---|---|---|
items | Vector | The parts. |
The example for this item is shown with the ones above: net-mime-Multipart.salam
MultipartWriter
struct MultipartWriter| Name | Type | Description |
|---|---|---|
boundary | str = "" | The random boundary between the parts. |
buf | compress.ByteBuf | The body built so far. |
closed | bool = false | Whether it has been closed. |
The example for this item is shown with the ones above: net-mime-Multipart.salam
Functions
MimeExt
func MimeExt(ext: str): strpng; application/octet-stream for an unknown extension. About 25 common web extensions are known.The example for this item is shown with the ones above: net-mime-Types.salam
MimeType
func MimeType(p: str): strapplication/octet-stream when there is none or it is unknown.The example for this item is shown with the ones above: net-mime-Types.salam
StatusText
func StatusText(code: int): strNot Found for 404.The example for this item is shown with the ones above: net-mime-Types.salam
ParseContentType
func ParseContentType(header: str): ContentTypeContent-Type header.The example for this item is shown with the ones above: net-mime-Types.salam
ParamGet
func ParamGet(ct: ContentType, key: str): strThe example for this item is shown with the ones above: net-mime-Types.salam
ParseMultipart
func ParseMultipart(data: void*, n: int, boundary: str): MultipartPartsmultipart/form-data body in memory into parts at the given boundary; for taking in uploaded files. Binary bodies are kept whole in each part's ByteBuf.The example for this item is shown with the ones above: net-mime-Multipart.salam
PartIndex
func PartIndex(parts: MultipartParts, name: str): intparts.items, or -1 when there is none.The example for this item is shown with the ones above: net-mime-Multipart.salam
PartValue
func PartValue(parts: MultipartParts, name: str): strThe example for this item is shown with the ones above: net-mime-Multipart.salam
FreeMultipart
func FreeMultipart(parts &: MultipartParts)ParseMultipart.The example for this item is shown with the ones above: net-mime-Multipart.salam
NewMultipartWriter
func NewMultipartWriter(): MultipartWriterThe example for this item is shown with the ones above: net-mime-Multipart.salam
ContentTypeHeader
func ContentTypeHeader(w: MultipartWriter): strContent-Type header value to send with this body.The example for this item is shown with the ones above: net-mime-Multipart.salam
WriteField
func WriteField(w &: MultipartWriter, name: str, value: str)The example for this item is shown with the ones above: net-mime-Multipart.salam
WriteFileBytes
func WriteFileBytes(w &: MultipartWriter, name: str, filename: str, content_type: str, data: void*, n: int)The example for this item is shown with the ones above: net-mime-Multipart.salam
WriteFileStr
func WriteFileStr(w &: MultipartWriter, name: str, filename: str, content_type: str, data: str)The example for this item is shown with the ones above: net-mime-Multipart.salam
CloseWriter
func CloseWriter(w &: MultipartWriter): compress.ByteBufThe example for this item is shown with the ones above: net-mime-Multipart.salam