TL;DR: Given an arbitrary filename as a Go string
value, what's the best way to create a Content-Disposition
header field that specifies that filename?
I'm writing a Go net/http handler, and I want to set the Content-Disposition
header field to specify a filename that the browser should use when saving the file. According to MDN, the syntax is:
Content-Disposition: attachment; filename="filename.jpg"
and "filename.jpg"
in an HTTP "quoted-string". However, I don't see any mention of "quote" in the net/http docs. Only mentions of HTML and URL escaping.
Is quoted-string the same as or at least compatible with URL escaping? Can I just use url.QueryEscape or url.PathEscape for this? If so, which one should I use, or are they both safe for this purpose? HTTP quoted-string looks similar to URL escaping, but I can't immediately find anything saying whether they're compatible, or if there are edge cases to worry about.
Alternatively, is there a higher-level package I should be using instead that can handle the details of constructing HTTP header field values that contain parameters like this?
The value of the HTTP request header you want to set can only contain: Alphanumeric characters: a - z and A - Z. The following special characters: _ :;.,\/"'?!(){}[]@<>=-+*#$&`|~^%
- This is a secure coding best practice to encode the HTTP headers (and other contexts like db queries, URLs, file paths etc) that may contain sensitive or untrusted data.
A quoted string is a string constant that is surrounded by quotation marks. Use the Quoted String segment whenever you see a reference to a quoted string in a syntax diagram.
In a regular HTTP response, the Content-Disposition response header is a header indicating if the content is expected to be displayed inline in the browser, that is, as a Web page or as part of a Web page, or as an attachment, that is downloaded and saved locally.
Last Updated : 25 Aug, 2021 The HTTP headers are used to pass additional information between the clients and the server through the request and response header. All the headers are case-insensitive, headers fields are separated by colon, key-value pairs in clear-text string format. The end of the header section denoted by an empty field header.
The From request-header field contains an Internet e-mail address for the human user who controls the requesting user agent. Following is a simple example: This header field may be used for logging purposes and as a means for identifying the source of invalid or unwanted requests.
what characters are allowed in HTTP header values? Bookmark this question. Show activity on this post. After studying HTTP/1.1 standard, specifically page 31 and related I came to conclusion that any 8-bit octet can be present in HTTP header value. I.e. any character with code from [0,255] range.
You can specify multiple headers separated by commas and a value of asterisk "*" signals that unspecified parameters are not limited to the request-headers. Following is a simple example: Here field names are case-insensitive. The WWW-Authenticate response-header field must be included in 401 (Unauthorized) response messages.
HTTP quoted-string is defined in RFC 7230:
quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE
qdtext = HTAB / SP /%x21 / %x23-5B / %x5D-7E / obs-text
obs-text = %x80-FF
quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text )
where VCHAR is any visible ASCII character.
The following function quotes per the RFC:
// quotedString returns s quoted per quoted-string in RFC 7230.
func quotedString(s string) (string, error) {
var result strings.Builder
result.Grow(len(s) + 2) // optimize for case where no \ are added.
result.WriteByte('"')
for i := 0; i < len(s); i++ {
b := s[i]
if (b < ' ' && b != '\t') || b == 0x7f {
return "", fmt.Errorf("invalid byte %0x", b)
}
if b == '\\' || b == '"' {
result.WriteByte('\\')
}
result.WriteByte(b)
}
result.WriteByte('"')
return result.String(), nil
}
Use the function like this:
qf, err := quotedString(f)
if err != nil {
// handle invalid byte in filename f
}
header.Set("Content-Disposition", "attachment; filename=" + qf)
It may be convenient to fix invalid bytes instead of reporting an error. It's probably a good idea to clean up invalid UTF8 as well. Here's a quote function that does that:
// cleanQuotedString returns s quoted per quoted-string in RFC 7230 with invalid
// bytes and invalid UTF8 replaced with _.
func cleanQuotedString(s string) string {
var result strings.Builder
result.Grow(len(s) + 2) // optimize for case where no \ are added.
result.WriteByte('"')
for _, r := range s {
if (r < ' ' && r != '\t') || r == 0x7f || r == 0xfffd {
r = '_'
}
if r == '\\' || r == '"' {
result.WriteByte('\\')
}
result.WriteRune(r)
}
result.WriteByte('"')
return result.String()
}
If you know that the filename does not contain invalid bytes, then copy the following code from the mime/multipart package source:
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
func escapeQuotes(s string) string {
return quoteEscaper.Replace(s)
}
The standard library code is similar to the code in Steven Penny's answer, but the standard library code allocates and builds the replacer once instead of on each invocation of escapeQuotes
.
One way is using the multipart
package [1]:
package main
import (
"mime/multipart"
"strings"
)
func main() {
b := new(strings.Builder)
m := multipart.NewWriter(b)
defer m.Close()
m.CreateFormFile("attachment", "filename.jpg")
print(b.String())
}
Result:
--81200ce57413eafde86bb95b1ba47121862043451ba5e55cda9af9573277
Content-Disposition: form-data; name="attachment"; filename="filename.jpg"
Content-Type: application/octet-stream
or you can use this function, based on the Go source code [2]:
package escape
import "strings"
func escapeQuotes(s string) string {
return strings.NewReplacer(`\`, `\\`, `"`, `\"`).Replace(s)
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With