Standard library · Network

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. MimeExt knows 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, ttf and docx included, becomes application/octet-stream, so handle the extra types your site serves before calling it.
  • Lookups and comparisons are case sensitive. MimeType does not lower the extension, ParseContentType keeps the case of the media type and the parameter names, and ParamGet matches the name exactly, so Charset=UTF-8 is not found by "charset". Lower both sides before comparing.
  • Only three types get a charset: text/html, text/plain and text/markdown come with charset=utf-8; CSS and JavaScript do not.
  • Binary uploads need the bytes. ParseMultipart works on raw memory and keeps each body in a ByteBuf, but if the request body has passed through a str on 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.

net-mime-guide-static.salam
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)"
end
Outputindex.html -> Content-Type: text/html; charset=utf-8 app.js -> Content-Type: application/javascript style.css -> Content-Type: text/css logo.svg -> Content-Type: image/svg+xml photo.JPG -> Content-Type: image/jpeg fonts/vazir.woff2 -> Content-Type: font/woff2 report.pdf -> Content-Type: application/pdf data.bin -> Content-Type: application/octet-stream README -> Content-Type: application/octet-stream application/octet-stream (without lower)

The 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.

net-mime-guide-content-type.salam
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("")
end
OutputJSON body, charset none JSON body, charset utf-8 URL encoded form upload, boundary abc123 plain text, charset windows-1256 JSON body, charset none unknown: []

Media 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.

net-mime-guide-upload.salam
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)
end
Outputfield title = Trip to Shiraz field album = 2026 file notes notes.txt text/plain; charset=utf-8 18 bytes file photo eram.jpg image/jpeg 17 bytes 415 Unsupported Media Type

When 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.

net-mime-guide-roundtrip.salam
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
end
Output200 user: maryam todo: buy bread

Two 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
One parameter.
NameTypeDescription
keystr = ""The name, in the case it was sent.
valuestr = ""The value.
net-mime-Types.salam
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
end
Outputimage/png application/json application/octet-stream text/html; charset=utf-8 text/css OK Not Found Service Unavailable text/html 2 UTF-8 q 0.9

ContentTypeParams

struct ContentTypeParams
A list of parameters.
NameTypeDescription
itemsVectorThe parameters.

The example for this item is shown with the ones above: net-mime-Types.salam

ContentType

struct ContentType
A parsed Content-Type header.
NameTypeDescription
media_typestr = ""The media type, trimmed but in the case it was sent.
paramsContentTypeParamsThe parameters, such as charset.

The example for this item is shown with the ones above: net-mime-Types.salam

MultipartHeader

struct MultipartHeader
One header of a part.
NameTypeDescription
keystr = ""The name.
valuestr = ""The value.
net-mime-Multipart.salam
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)
end
Outputtrue true true name [] [] Maryam 1 note [note.txt] [text/plain] hello 2 pic [a.png] [image/png] PNG 2 Content-Disposition 1 -1 Maryam []

MultipartHeaders

struct MultipartHeaders
A list of headers.
NameTypeDescription
itemsVectorThe headers.

The example for this item is shown with the ones above: net-mime-Multipart.salam

MultipartPart

struct MultipartPart
One part of a multipart body.
NameTypeDescription
namestr = ""The name of the form field.
filenamestr = ""The file name, when the part is a file.
content_typestr = ""The content type of the part.
headersMultipartHeadersEvery header of the part.
bodycompress.ByteBufThe contents of the part.

The example for this item is shown with the ones above: net-mime-Multipart.salam

MultipartParts

struct MultipartParts
A list of parts.
NameTypeDescription
itemsVectorThe parts.

The example for this item is shown with the ones above: net-mime-Multipart.salam

MultipartWriter

struct MultipartWriter
A builder of a multipart body.
NameTypeDescription
boundarystr = ""The random boundary between the parts.
bufcompress.ByteBufThe body built so far.
closedbool = falseWhether 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): str
The media type for a file extension, without the dot and in lower case, such as png; 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): str
The media type from a file path, by its extension, which must already be lower case; application/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): str
The standard text of an HTTP status code, such as Not Found for 404.

The example for this item is shown with the ones above: net-mime-Types.salam

ParseContentType

func ParseContentType(header: str): ContentType
Parses a Content-Type header.

The example for this item is shown with the ones above: net-mime-Types.salam

ParamGet

func ParamGet(ct: ContentType, key: str): str
The value of one parameter, quotes removed; an empty string when there is none. The name must match exactly, case included.

The example for this item is shown with the ones above: net-mime-Types.salam

ParseMultipart

func ParseMultipart(data: void*, n: int, boundary: str): MultipartParts
Splits a multipart/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): int
The position of the first part with the given field name in parts.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): str
The body of the first part with the given field name as text, for an ordinary form field; empty when there is no such part.

The example for this item is shown with the ones above: net-mime-Multipart.salam

FreeMultipart

func FreeMultipart(parts &: MultipartParts)
Frees the body of every part and the list itself; call it once you are done with the parts from ParseMultipart.

The example for this item is shown with the ones above: net-mime-Multipart.salam

NewMultipartWriter

func NewMultipartWriter(): MultipartWriter
A new builder with a random boundary.

The example for this item is shown with the ones above: net-mime-Multipart.salam

ContentTypeHeader

func ContentTypeHeader(w: MultipartWriter): str
The Content-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)
Adds one text form field.

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)
Adds one file from memory.

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)
Adds one text file.

The example for this item is shown with the ones above: net-mime-Multipart.salam

CloseWriter

func CloseWriter(w &: MultipartWriter): compress.ByteBuf
Writes the closing boundary and returns the complete body.

The example for this item is shown with the ones above: net-mime-Multipart.salam