英文:
Handle file uploading with go
问题
我最近开始学习使用Go,所以我还是个新手,如果我犯了太多错误,请原谅。我一直在尝试解决这个问题很长时间了,但我就是不明白发生了什么。在我的main.go文件中,我有一个main函数:
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/submit/", submit)
log.Fatal(http.ListenAndServe(":8080", nil))
}
handler函数的代码如下:
func handler(w http.ResponseWriter, r *http.Request) {
data, _ := ioutil.ReadFile("web/index.html")
w.Write(data)
}
我知道这不是提供网站的最佳方式。submit函数的代码如下:
func submit(w http.ResponseWriter, r *http.Request) {
log.Println("METHOD IS " + r.Method + " AND CONTENT-TYPE IS " + r.Header.Get("Content-Type"))
r.ParseMultipartForm(32 << 20)
file, header, err := r.FormFile("uploadFile")
if err != nil {
json.NewEncoder(w).Encode(Response{err.Error(), true})
return
}
defer file.Close()
out, err := os.Create("/tmp/file_" + time.Now().String() + ".png")
if err != nil {
json.NewEncoder(w).Encode(Response{err.Error(), true})
return
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
json.NewEncoder(w).Encode(Response{err.Error(), true})
return
}
json.NewEncoder(w).Encode(Response{"File '" + header.Filename + "' submitted successfully", false})
}
问题是当执行submit函数时,r.Method
的值是GET
,r.Header.Get("Content-Type")
的值是空字符串,然后继续执行直到第一个if语句,其中r.FormFile返回以下错误:
request Content-Type isn't multipart/form-data
我不明白为什么r.Method总是GET,而且没有Content-Type。我尝试以多种不同的方式编写index.html,但r.Method始终是GET,Content-Type为空。下面是上传文件的index.html中的函数代码:
function upload() {
var formData = new FormData();
formData.append('uploadFile', document.querySelector('#file-input').files[0]);
fetch('/submit', {
method: 'post',
headers: {
"Content-Type": "multipart/form-data"
},
body: formData
}).then(function json(response) {
return response.json()
}).then(function(data) {
window.console.log('Request succeeded with JSON response', data);
}).catch(function(error) {
window.console.log('Request failed', error);
});
}
以下是HTML代码:
<input id="file-input" type="file" name="uploadFile" />
请注意,<input>
标签不在<form>
标签内,我认为这可能是问题所在,所以我将函数和HTML都更改为以下内容:
function upload() {
fetch('/submit', {
method: 'post',
headers: {
"Content-Type": "multipart/form-data"
},
body: new FormData(document.querySelector('#form'))
}).then(function json(response) {
return response.json()
}).then(function(data) {
window.console.log('Request succeeded with JSON response', data);
}).catch(function(error) {
window.console.log('Request failed', error);
});
}
<form id="form" method="post" enctype="multipart/form-data" action="/submit">
<input id="file-input" type="file" name="uploadFile" />
</form>
但这也没有起作用。我在Google上搜索了如何使用fetch()和如何从Go接收文件上传的方法,发现它们与我的代码非常相似,我不知道我做错了什么。
更新:
在使用curl -v -F 'uploadFile=@"C:/Users/raul-/Desktop/test.png"' http://localhost:8080/submit
命令后,我得到以下输出:
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> POST /submit HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.45.0
> Accept: */*
> Content-Length: 522
> Expect: 100-continue
> Content-Type: multipart/form-data; boundary=---------------------------a17d4e54fcec53f8
>
< HTTP/1.1 301 Moved Permanently
< Location: /submit/
< Date: Wed, 18 Nov 2015 14:48:38 GMT
< Content-Length: 0
< Content-Type: text/plain; charset=utf-8
* HTTP error before end of send, stop sending
<
* Closing connection 0
当我使用curl命令时,我在运行go run main.go
的控制台上没有任何输出。
英文:
I've started playing with go very recently so I'm still a noob, sorry if I make too many mistakes. I've been trying to fix this for a long time but I just don't understand what's going on. In my main.go file I have a main function:
func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/submit/", submit)
log.Fatal(http.ListenAndServe(":8080", nil))
}
The handler function looks like this:
func handler(w http.ResponseWriter, r *http.Request) {
data, _ := ioutil.ReadFile("web/index.html")
w.Write(data)
}
I know this is not the best way to serve a website
The submit function looks like this:
func submit(w http.ResponseWriter, r *http.Request) {
log.Println("METHOD IS " + r.Method + " AND CONTENT-TYPE IS " + r.Header.Get("Content-Type"))
r.ParseMultipartForm(32 << 20)
file, header, err := r.FormFile("uploadFile")
if err != nil {
json.NewEncoder(w).Encode(Response{err.Error(), true})
return
}
defer file.Close()
out, err := os.Create("/tmp/file_" + time.Now().String() + ".png")
if err != nil {
json.NewEncoder(w).Encode(Response{err.Error(), true})
return
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
json.NewEncoder(w).Encode(Response{err.Error(), true})
return
}
json.NewEncoder(w).Encode(Response{"File '" + header.Filename + "' submited successfully", false})
}
The problem is when the submit function is executed, r.Method
is GET
and r.Header.Get("Content-Type")
is an empty string, then it continues until the first if where r.FormFile returns the following error:
request Content-Type isn't multipart/form-data
I don't understand why r.Method is always GET and there's no Content-Type. I've tried to do the index.html in many different ways but r.Method is always GET and Content-Type is empty. Here's the function in index.html that uploads a file:
function upload() {
var formData = new FormData();
formData.append('uploadFile', document.querySelector('#file-input').files[0]);
fetch('/submit', {
method: 'post',
headers: {
"Content-Type": "multipart/form-data"
},
body: formData
}).then(function json(response) {
return response.json()
}).then(function(data) {
window.console.log('Request succeeded with JSON response', data);
}).catch(function(error) {
window.console.log('Request failed', error);
});
}
And here's the HTML:
<input id="file-input" type="file" name="uploadFile" />
Note that the <input> tag is not inside a <form> tag, I thought that could be the problem so I changed both the function and the HTML to something like this:
function upload() {
fetch('/submit', {
method: 'post',
headers: {
"Content-Type": "multipart/form-data"
},
body: new FormData(document.querySelector('#form')
}).then(function json(response) {
return response.json()
}).then(function(data) {
window.console.log('Request succeeded with JSON response', data);
}).catch(function(error) {
window.console.log('Request failed', error);
});
}
<form id="form" method="post" enctype="multipart/form-data" action="/submit"><input id="file-input" type="file" name="uploadFile" /></form>
But that didn't work neither. I've searched with Google how to use fetch() and how to receive a file upload from go and I've seen that they are pretty similar to mine, I don't know what I'm doing wrong.
UPDATE:
After using curl -v -F 'uploadFile=@\"C:/Users/raul-/Desktop/test.png\"' http://localhost:8080/submit
I get the following output:
* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> POST /submit HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.45.0
> Accept: */*
> Content-Length: 522
> Expect: 100-continue
> Content-Type: multipart/form-data; boundary=---------------------------a17d4e54fcec53f8
>
< HTTP/1.1 301 Moved Permanently
< Location: /submit/
< Date: Wed, 18 Nov 2015 14:48:38 GMT
< Content-Length: 0
< Content-Type: text/plain; charset=utf-8
* HTTP error before end of send, stop sending
<
* Closing connection 0
The console where I'm running go run main.go
outputs nothing when using curl.
答案1
得分: 15
我成功解决了我的问题,以下是解决方法,以便其他人需要时参考。感谢 @JiangYD 提供使用 curl 测试服务器的提示。
简短回答
- 我写了
http.HandleFunc("/submit/", submit)
,但我发送的是一个 POST 请求到/submit
(注意缺少的斜杠)<< 这很重要,因为涉及到重定向 - 不要自己指定 Content-Type,浏览器会自动处理
详细回答
我按照 @JiangYD 的建议使用 curl 测试服务器,我更新了我的回答并附上了响应。我发现奇怪的是有一个 301 重定向,因为我没有设置它,所以我决定使用以下 curl 命令:
curl -v -F 'uploadFile=@"C:/Users/raul-/Desktop/test.png"' -L http://localhost:8080/submit
(注意 -L)这样 curl 就会跟随重定向,但它仍然失败了,因为在重定向时,curl 从 POST 请求切换到了 GET 请求,但通过这个响应,我发现请求 /submit
被重定向到了 /submit/
,我记得在 main
函数中是这样写的。
修复了这个问题后,它仍然失败,响应是 http: no such file
,通过查看 net/http
代码,我发现这意味着该字段不存在,所以我快速测试了获取到的所有字段名:
for k, _ := range r.MultipartForm.File {
log.Println(k)
}
我得到的字段名是 uploadFile
,我在 curl 命令中去掉了单引号,现在它可以完美地上传文件了。
但问题还没有结束,我现在知道服务器是正常工作的,因为我可以使用 curl
上传文件,但是当我尝试通过托管的网页上传文件时,出现了错误:no multipart boundary param in Content-Type
。
所以我发现我应该在头部中包含 boundary,我将 fetch 更改为以下内容:
fetch('/submit', {
method: 'post',
headers: {
"Content-Type": "multipart/form-data; boundary=------------------------" + boundary
}, body: formData})
我像这样计算 boundary:
var boundary = Math.random().toString().substr(2);
但是我仍然得到一个错误:multipart: NextPart: EOF
。那么如何计算 boundary 呢?我阅读了规范 https://html.spec.whatwg.org/multipage/forms.html#multipart/form-data-encoding-algorithm,并发现 boundary 是由编码文件的算法计算的,在我的情况下是 FormData,FormData API 没有提供获取 boundary 的方法,但我发现如果你不指定它,浏览器会自动添加 multipart/form-data
的 Content-Type 和 boundary,所以我从 fetch
调用中移除了 headers 对象,现在它终于可以工作了!
英文:
I managed to solve my problem, so here it is in case someone else needs it. And thanks @JiangYD for the tip of using curl to test the server.
TL;DR
- I wrote
http.HandleFunc("/submit/", submit)
but I was making a POST request to/submit
(note the missing slash) << This is important because of redirections - Don't specify the Content-Type yourself, the browser will do it for you
LONG ANSWER
I did as @JiangYD said and used curl to test the server, I updated my answer with the response. I found odd that there was a 301 Redirect since I didn't put it there, I decided to use the following curl command
curl -v -F 'uploadFile=@\"C:/Users/raul-/Desktop/test.png\"' -L http://localhost:8080/submit
(note the -L) That way curl followed the redirect, though it failed again because, when redirecting, curl switched from POST to GET but with that response I found out that the request to /submit
was being redirected to /submit/
and I remembered that's how I wrote it in the main
function.
After fixing that it still failed, the response was http: no such file
and by looking at the net/http
code I found that it meant the field didn't exist, so I did a quick test iterating over all the field names obtained:
for k, _ := range r.MultipartForm.File {
log.Println(k)
}
I was getting 'uploadFile
as the field name, I removed the single quotes in the curl command and now it uploaded the file perfectly
But it doesn't end here, I now knew the server was working correctly because I could upload a file using curl
but when I tried uploading it through the hosted web page I got an error: no multipart boundary param in Content-Type
.
So I found out I was suppose to include the boundary in the header, I changed fetch to something like this:
fetch('/submit', {
method: 'post',
headers: {
"Content-Type": "multipart/form-data; boundary=------------------------" + boundary
}, body: formData})
I calculate the boundary like this:
var boundary = Math.random().toString().substr(2);
But I still got an error: multipart: NextPart: EOF
So how do you calculate the boundary? I read the spec https://html.spec.whatwg.org/multipage/forms.html#multipart/form-data-encoding-algorithm and found out the boundary is calculated by the algorithm that encodes the file, which in my case is FormData, the FormData API doesn't expose a way to get that boundary but I found out that the browser adds the Content-Type with multipart/form-data
and the boundary automatically if you don't specify it so I removed the headers object from the fetch
call and now it finally works!
答案2
得分: 0
移除头部实际上是有效的,特别是在通过fetch或axios发送请求时。
axios.post(
endpoint + "/api/v1/personalslip",
{
newSlip
},
{
}
)
.then(res => {
console.log(res);
});
英文:
Removing the header altogether actually works. Especially when sending the request via fetch or axios.
axios.post(
endpoint + "/api/v1/personalslip",
{
newSlip
},
{
}
)
.then(res => {
console.log(res);
});
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论