从服务器端下载PDF在golang中无法正常工作。

huangapple go评论84阅读模式
英文:

PDF download is not working from serverside in golang

问题

我已经在服务器端(使用golang)创建了一个PDF文件,然后我想通过API调用下载该PDF文件。我使用了Ajax的POST请求,该请求直接进入以下的ExportReport处理程序。但是我下载的PDF文档是空白页。

这个错误是由于请求头中的Content-Length设置引起的。

错误信息如下:

 http: wrote more than the declared Content-Length
2016/12/20 14:37:39 http: multiple response.WriteHeader calls

这个错误导致了PDF下载的问题,请查看我的代码片段。

func ExportReport(w http.ResponseWriter, r *http.Request) *core_commons.AppError {
    url := "https://mydomainname/reporting/repository/dashboard.pdf"

    timeout := time.Duration(5) * time.Second
    cfg := &tls.Config{
        InsecureSkipVerify: true,
    }
    transport := &http.Transport{
        TLSClientConfig:       cfg,
        ResponseHeaderTimeout: timeout,
        Dial: func(network, addr string) (net.Conn, error) {
            return net.DialTimeout(network, addr, timeout)
        },
        DisableKeepAlives: true,
    }

    client := &http.Client{
        Transport: transport,
    }
    resp, err := client.Get(url)
    if err != nil {
        fmt.Println(err)
    }
    defer resp.Body.Close()

    w.Header().Set("Content-Disposition", "attachment; filename=dashboard.pdf")
    w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
    w.Header().Set("Content-Length", r.Header.Get("Content-Length"))

    _, err = io.Copy(w, resp.Body)
    if err != nil {
        fmt.Println(err)
    }
    return nil
}

以下是如何调用Ajax请求的示例。

$.ajax({
    type: "POST",
    url: '/reporting/api/report/export',
    data: JSON.stringify(payload),
    contentType: 'application/pdf',
    success: function(response, status, xhr) {
        // 检查文件名
        var filename = "";
        var disposition = xhr.getResponseHeader('Content-Disposition');
        if (disposition && disposition.indexOf('attachment') !== -1) {
            var filenameRegex = /filename[^;=\n]*=(([\'"]).*?\2|[^;\n]*)/;
            var matches = filenameRegex.exec(disposition);
            if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
        }

        var type = xhr.getResponseHeader('Content-Type');
        var blob = new Blob([response], { type: type });

        if (typeof window.navigator.msSaveBlob !== 'undefined') {
            // IE的解决方法,用于“HTML7007: 一个或多个Blob URL已被关闭,因为它们所创建的Blob已被释放。这些URL将不再解析,因为支持URL的数据已被释放。”
            window.navigator.msSaveBlob(blob, filename);
        } else {
            var URL = window.URL || window.webkitURL;
            var downloadUrl = URL.createObjectURL(blob);

            if (filename) {
                // 使用HTML5的a[download]属性指定文件名
                var a = document.createElement("a");
                // Safari尚不支持此功能
                if (typeof a.download === 'undefined') {
                    window.location = downloadUrl;
                } else {
                    a.href = downloadUrl;
                    a.download = filename;
                    document.body.appendChild(a);
                    a.click();
                }
            } else {
                window.location = downloadUrl;
            }

            setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // 清理
        }
    }
});
英文:

I have created pdf in serverside(golang) then I want to download that pdf thorugh the api call.I have used ajax post request. that request direct into following ExportReport handlder. but my downloaded pdf document is blank page.
There is error happen because of the Content-Length setting on request header
Error is :

 http: wrote more than the declared Content-Length
2016/12/20 14:37:39 http: multiple response.WriteHeader calls

This error broken down pdf download.please go though my code snippets.

func ExportReport(w http.ResponseWriter, r *http.Request) *core_commons.AppError {
url := "https://mydomainname/reporting/repository/dashboard.pdf"
timeout := time.Duration(5) * time.Second
cfg := &tls.Config{
InsecureSkipVerify: true,
}
transport := &http.Transport{
TLSClientConfig:       cfg,
ResponseHeaderTimeout: timeout,
Dial: func(network, addr string) (net.Conn, error) {
return net.DialTimeout(network, addr, timeout)
},
DisableKeepAlives: true,
}
client := &http.Client{
Transport: transport,
}
resp, err := client.Get(url)
if err != nil {
fmt.Println(err)
}
defer resp.Body.Close()
w.Header().Set("Content-Disposition", "attachment; filename=dashboard.pdf")
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
w.Header().Set("Content-Length", r.Header.Get("Content-Length"))
_, err = io.Copy(w, resp.Body)
if err != nil {
fmt.Println(err)
}
return nil
}

Following are the how to invoke ajax request.

$.ajax({
type: "POST",
url: '/reporting/api/report/export',
data: JSON.stringify(payload),
contentType: 'application/pdf',
success: function(response, status, xhr) {
// check for a filename
var filename = "";
var disposition = xhr.getResponseHeader('Content-Disposition');
if (disposition && disposition.indexOf('attachment') !== -1) {
var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
var matches = filenameRegex.exec(disposition);
if (matches != null && matches[1]) filename = matches[1].replace(/['"]/g, '');
}
var type = xhr.getResponseHeader('Content-Type');
var blob = new Blob([response], { type: type });
if (typeof window.navigator.msSaveBlob !== 'undefined') {
// IE workaround for "HTML7007: One or more blob URLs were revoked by closing the blob for which they were created. These URLs will no longer resolve as the data backing the URL has been freed."
window.navigator.msSaveBlob(blob, filename);
} else {
var URL = window.URL || window.webkitURL;
var downloadUrl = URL.createObjectURL(blob);
if (filename) {
// use HTML5 a[download] attribute to specify filename
var a = document.createElement("a");
// safari doesn't support this yet
if (typeof a.download === 'undefined') {
window.location = downloadUrl;
} else {
a.href = downloadUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
}
} else {
window.location = downloadUrl;
}
setTimeout(function () { URL.revokeObjectURL(downloadUrl); }, 100); // cleanup
}
}
});

答案1

得分: 2

看一下这两行代码:

w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
w.Header().Set("Content-Length", r.Header.Get("Content-Length"))

你想要设置与获取PDF时相同的内容类型和长度,但是r请求是与你所提供的请求相关联的!应该是:

w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
w.Header().Set("Content-Length", resp.Header.Get("Content-Length"))

还要注意,不能保证你调用的URL会设置Content-Length,所以只有在它非零时才在响应中设置它。还要注意,发送的内容长度可能不正确,所以要小心处理。还要注意,内容长度头部会被net/http包自动解析并存储在响应中,你可以直接使用Response.ContentLength

如果设置了内容长度,net/http包将不允许你发送超过指定长度的字节。尝试写入更多字节会导致错误:

> http: wrote more than the declared Content-Length

这个小例子证明/验证了这一点:

func h(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", "1")
fmt.Println(w.Write([]byte("hi")))
}
func main() {
http.HandleFunc("/", h)
panic(http.ListenAndServe(":8080", nil))
}

处理程序h()写入了2个字节,但在内容长度中只指定了1个字节。如果将其更改为2,一切正常。

所以你应该先检查r.Header.Get("Content-Length"),如果它不是空字符串且是大于0的数字,才设置它。

如果接收到的内容长度丢失,但你仍然想在响应中指示它,那么你只能先将内容读入缓冲区,然后在发送之前检查并设置其长度。

此外,你忽略了检查HTTP GET请求是否成功。你的注释表明它是一个错误页面。首先检查一下:

resp, err := client.Get(url)
if err != nil {
fmt.Println(err)
http.Error(w, "Can't serve PDF.", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
http.Error(w, "Can't serve PDF.", http.StatusInternalServerError)
return
}
英文:

Look at these 2 lines:

w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
w.Header().Set("Content-Length", r.Header.Get("Content-Length"))

You want to set the same content type and length you get when getting the PDF, but the r request is the one associated with the request you serve! It should be:

w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
w.Header().Set("Content-Length", resp.Header.Get("Content-Length"))

And also note that there is no guarantee that the URL you call will set the Content-Length, and so you should only set it in your response if it's non-zero. Also note that there is also no guarantee that the content length it sends is correct, so you should handle that with care. Also note that the content length header is automatically parsed by the net/http package and is stored in the response, you can use just that: Response.ContentLength.

If you set the content length, the net/http package will not allow you to send more bytes than indicated. Attempting to write more gives you the error:

> http: wrote more than the declared Content-Length

This little example proves / verifies it:

func h(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", "1")
fmt.Println(w.Write([]byte("hi")))
}
func main() {
http.HandleFunc("/", h)
panic(http.ListenAndServe(":8080", nil))
}

The handler h() writes 2 bytes, but indicates only 1 in content length. If you change that to 2, everything works.

So what you should do is first check r.Header.Get("Content-Length") if it's not the empty string and is a number greater than 0; and only set it if so.

If the received content length is missing and you still want to indicate it in your response, then you have no other choice but to first read the content into a buffer, whose length you can check and set prior to sending it.

Also you omit checking if the HTTP GET request succeeded. Your comment indicates that it was an error page. Check that first:

resp, err := client.Get(url)
if err != nil {
fmt.Println(err)
http.Error(w, "Can't serve PDF.", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
http.Error(w, "Can't serve PDF.", http.StatusInternalServerError)
return
}

答案2

得分: 1

package main
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"path"
)
func main() {
fmt.Println("Starting transform download sever at http://127.0.0.1:2333")
http.HandleFunc("/", HandleClient)
err := http.ListenAndServe(":2333", nil)
if err != nil {
fmt.Println(err)
}
}
func HandleClient(writer http.ResponseWriter, request *http.Request) {
// 首先检查URL中是否设置了Get参数
encoded := request.URL.Query().Get("url")
if encoded == "" {
// Get参数未设置,返回400错误请求
http.Error(writer, "在URL中未指定Get参数'url'。", 500)
return
}
decoded, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
http.Error(writer, "base64解码错误", 501)
return
}
fileUrl := string(decoded)
filename, err := GetFilename(fileUrl)
if err != nil {
http.Error(writer, "URL错误", 502)
return
}
resp, err := http.Get(fileUrl)
if err != nil {
http.Error(writer, "URL错误", 502)
return
}
defer resp.Body.Close()
writer.Header().Set("Content-Disposition", "attachment; filename="+filename)
writer.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
writer.Header().Set("Content-Length", resp.Header.Get("Content-Length"))
_, err = io.Copy(writer, resp.Body)
if err != nil {
http.Error(writer, "远程服务器错误", 503)
return
}
return
}
func GetFilename(inputUrl string) (string, error) {
u, err := url.Parse(inputUrl)
if err != nil {
return "", err
}
u.RawQuery = ""
return path.Base(u.String()), nil
}

使用方式:http://127.0.0.1:2333/?url=base64编码后的URL

英文:
package main
import (
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
"path"
)
func main() {
fmt.Println("Starting transform download sever at http://127.0.0.1:2333")
http.HandleFunc("/", HandleClient)
err := http.ListenAndServe(":2333", nil)
if err != nil {
fmt.Println(err)
}
}
func HandleClient(writer http.ResponseWriter, request *http.Request) {
//First of check if Get is set in the URL
encoded := request.URL.Query().Get("url")
if encoded == "" {
//Get not set, send a 400 bad request
http.Error(writer, "Get 'url' not specified in url.", 500)
return
}
decoded, err  := base64.StdEncoding.DecodeString(encoded)
if err != nil {
http.Error(writer, "base64 decode error", 501)
return
}
fileUrl := string(decoded)
filename, err := GetFilename(fileUrl)
if err != nil {
http.Error(writer, "error url", 502)
return
}
resp, err := http.Get(fileUrl)
if err != nil {
http.Error(writer, "error url", 502)
return
}
defer resp.Body.Close()
writer.Header().Set("Content-Disposition", "attachment; filename="+filename)
writer.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
writer.Header().Set("Content-Length", resp.Header.Get("Content-Length"))
_, err = io.Copy(writer, resp.Body)
if err != nil {
http.Error(writer, "Remote server error", 503)
return
}
return
}
func GetFilename(inputUrl string) (string, error) {
u, err := url.Parse(inputUrl)
if err != nil {
return "", err
}
u.RawQuery = ""
return path.Base(u.String()), nil
}

Use like http://127.0.0.1:2333/?url=base64encoded(url)

huangapple
  • 本文由 发表于 2016年12月20日 17:05:52
  • 转载请务必保留本文链接:https://go.coder-hub.com/41238407.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定