PDF download is not working from serverside in gol

2019-06-10 15:44发布

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
        }
    }
});

2条回答
叛逆
2楼-- · 2019-06-10 16:10

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
}
查看更多
forever°为你锁心
3楼-- · 2019-06-10 16:11
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)

查看更多
登录 后发表回答