Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: mime/multipart: allow specifying content type in Writer.CreateFormFile #49329

Closed
x1unix opened this issue Nov 3, 2021 · 17 comments
Closed
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal
Milestone

Comments

@x1unix
Copy link

x1unix commented Nov 3, 2021

What version of Go are you using (go version)?

$ go version
go version go1.17.2 linux/amd64

Does this issue reproduce with the latest release?

Yes

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/x1unix/.cache/go-build"
GOENV="/home/x1unix/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/x1unix/go/pkg/mod"
GONOPROXY="git.tor.ph/*"
GONOSUMDB="git.tor.ph/*"
GOOS="linux"
GOPATH="/home/x1unix/go"
GOPRIVATE="git.tor.ph/*"
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go"
GOSUMDB="off"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.17.2"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS="-w"
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build1687643626=/tmp/go-build -gno-record-gcc-switches"

What did you do?

I'm using multipart.Writer to build request for uploading a file.
I had a case in which I need to set an appropriate content type for uploaded file in order to submit a correct request.

Unfortunately, CreateFormFile doesn't allow specifying
a custom content type or does any content type check but contains a hardcoded generic content type "application/octet-stream"

// CreateFormFile is a convenience wrapper around CreatePart. It creates
// a new form-data header with the provided field name and file name.
func (w *Writer) CreateFormFile(fieldname, filename string) (io.Writer, error) {
	h := make(textproto.MIMEHeader)
	h.Set("Content-Disposition",
		fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
			escapeQuotes(fieldname), escapeQuotes(filename)))
	h.Set("Content-Type", "application/octet-stream")
	return w.CreatePart(h)
}

The current workaround is to implement a custom CreateFormFile replacement with custom content type.

What did you expect to see?

I see at least one of possible solutions:

  • Filename-based content type detection (e.g. using http.DetectContentType)
  • Introduce a new method that allow specifying custom content type (like CreateFormFileWithContentType)

What did you see instead?

Content type is hardcoded in CreateFormFile method.

@seankhliao seankhliao added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Nov 4, 2021
@seankhliao
Copy link
Member

cc @neild @minux

@neild
Copy link
Contributor

neild commented Nov 4, 2021

CreateFormFile is a convenience wrapper, but it does seem like a gap in the package API that there's no function that sets the Content-Disposition field correctly while allowing the caller to specify the Content-Type.

Perhaps a ("mime/multipart".Writer).CreateFormFileWithContentType(fieldname, filename, contentType string) would be a reasonable addition. If someone wants to pursue adding this, the new function will need to follow the proposal process.

@x1unix
Copy link
Author

x1unix commented Nov 4, 2021

@neild ok, so what are my next steps to make a proposal?

@ianlancetaylor
Copy link
Contributor

@x1unix
Copy link
Author

x1unix commented Nov 5, 2021

@ianlancetaylor @neild

A non-proposal issue can be turned into a proposal by simply adding the proposal label

I added a proposal prefix to the issue, this should be legit according to the docs.
Do I need any additional steps to do?

@x1unix x1unix changed the title mime/multipart: hardcoded content type in Writer.CreateFormFile proposal: mime/multipart: allow specifying content type in Writer.CreateFormFile Nov 5, 2021
@gopherbot gopherbot added this to the Proposal milestone Nov 5, 2021
@ianlancetaylor ianlancetaylor added this to Incoming in Proposals (old) Nov 5, 2021
@ianlancetaylor
Copy link
Contributor

Do I need any additional steps to do?

No.

@neild
Copy link
Contributor

neild commented Nov 5, 2021

Concretizing the proposal:

package multipart

// CreateFormFileWithContentType is a convenience wrapper around CreatePart.
// It creates a new form-data header with the provided field name and file name.
func (w *Writer) CreateFormFileWithContentType(fieldname, filename, contentType string) (io.Writer, error) {}

@x1unix
Copy link
Author

x1unix commented Nov 6, 2021

@neild what do you think about options pattern for this method?

Just to add additional third argument with options like this:

type Option = func(h *textproto.MIMEHeader)

var WithContentType Option = ...
var WithContentRanges Option = ...

func (w *Writer) CreateFormFile(fieldname, filename string, options ...Option) (io.Writer, error)

This allows to set additional headers like Content-Ranges for partial upload, or other headers.
Also the method will be backward compatible.

@neild
Copy link
Contributor

neild commented Nov 8, 2021

@neild what do you think about options pattern for this method?

Seems heavyweight for this case.

If we want a more general-purpose helper function, then perhaps something like this would be useful:

// NewFormDataHeader creates a form-data header with the provided field name.
func NewFormHeader(fieldname, filename string) textproto.MIMEHeader {
  h := make(textproto.MIMEHeader)
  if filename == "" {
    h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"`, escapeQuotes(fieldname)))
  } else {
    h.Set("Content-Disposition", fmt.Sprintf(`form-data; name="%s"; filename="%s"`, escapeQuotes(fieldname), escapeQuotes(filename)))
  }
}

Usage:

h := multipart.NewFormHeader("field", "")
h.Set("Content-Rodent", "gopher")
wr, err := w.CreatePart(h)

@x1unix
Copy link
Author

x1unix commented Nov 10, 2021

@neild seems reasonable. Interesting also to hear other opinions.

@rsc
Copy link
Contributor

rsc commented Nov 10, 2021

This looks like a duplicate of #46771 ?

@rsc rsc moved this from Incoming to Active in Proposals (old) Nov 10, 2021
@rsc
Copy link
Contributor

rsc commented Nov 10, 2021

This proposal has been added to the active column of the proposals project
and will now be reviewed at the weekly proposal review meetings.
— rsc for the proposal review group

@x1unix
Copy link
Author

x1unix commented Nov 11, 2021

This looks like a duplicate of #46771 ?

@rsc looks like that this issue rather complements #46771. Original issue is about content disposition and this one is about content type header, but the solution should resolve both issues.

@rsc
Copy link
Contributor

rsc commented Dec 1, 2021

With #46771 accepted, this use case becomes:

h := make(textproto.MIMEHeader)
h.Set("Content-Disposition", FileContentDisposition(fieldname, filename)
h.Set("Content-Type", "application/pdf")
p, err := w.CreatePart(h)

Does that seem OK? It seems clearer and more extensible to do that than to have another constructor for each header we might want to set.

@x1unix
Copy link
Author

x1unix commented Dec 1, 2021

@rsc looks good to me. What do you think about idea to introduce constants for formdata-common headers like Content-Type, Content-Disposition, etc?

h := make(textproto.MIMEHeader)
h.Set(multipartHeader.ContentDisposition, FileContentDisposition(fieldname, filename)
h.Set(multipart.HeaderContentType, "application/pdf")
p, err := w.CreatePart(h)

@rsc
Copy link
Contributor

rsc commented Dec 8, 2021

We have avoided adding "Content-Type" as a named constant in net/http, so it doesn't seem like we should add it here either. We did reluctantly add things like http.MethodGet, but now we get many CLs changing "GET" to http.MethodGet, which is churn that makes the code harder to read. I don't think we want to go down that road again.

Given that #46771 takes care of this use case, closing as duplicate.

@rsc rsc moved this from Active to Declined in Proposals (old) Dec 8, 2021
@rsc
Copy link
Contributor

rsc commented Dec 8, 2021

This proposal is a duplicate of a previously discussed proposal, as noted above,
and there is no significant new information to justify reopening the discussion.
The issue has therefore been declined as a duplicate.
— rsc for the proposal review group

@rsc rsc closed this as completed Dec 8, 2021
@golang golang locked and limited conversation to collaborators Dec 8, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. Proposal
Projects
No open projects
Development

No branches or pull requests

6 participants