I am using net/http library in 'Go' to make an HTTP GET request. In the response, i get 12 headers. But when i run the exact same query through postman, i get 16 headers. One of those missing is 'Content-Encoding'. I understand this must be a CORS issue.
But since i have not set the header Accept-Encoding: gzip in my request, and i am still getting the gzip encoding in response, the Go transport is not automatically decompressing the response for me. So, i need to be able to manually detect the encoding and then decompress it. But, i cannot detect if the 'Content-Encoding' header is missing in the response.
Here is my code where i try to do this:
func calcDistanceAndDurationWithUberApi(originLat float64, originLon float64, destinationLat float64, destinationLon float64) (float64, float64, error) {
endpoint := "https://api.uber.com/v1.2/estimates/price"
parameters := fmt.Sprintf("?start_latitude=%v&start_longitude=%v&end_latitude=%v&end_longitude=%v", originLat, originLon, destinationLat, destinationLon)
req, err := http.NewRequest("GET", endpoint + parameters, nil)
if err != nil {
return 0, 0, err
}
req.Header.Add("Authorization", "Token " + getUberApiKey())
req.Header.Add("Accept-Language", "en_US")
req.Header.Add("Content-Type", "application/json")
httpClient := &http.Client{}
resp, err := httpClient.Do(req)
if err != nil {
return 0, 0, err
}
if resp.StatusCode != 200 {
return 0, 0, errors.NotFound("Response: %v", resp.StatusCode)
}
defer resp.Body.Close()
pretty.Println("- REQUEST: ")
pretty.Println(req)
// Check if server sent gzipped response. Decompress if yes.
var respReader io.ReadCloser
switch resp.Header.Get("Content-Encoding") {
case "gzip":
fmt.Println("Content-Encoding is gzip")
respReader, err = gzip.NewReader(resp.Body)
defer respReader.Close()
default:
fmt.Println("Content-Encoding is Not gzip")
respReader = resp.Body
}
pretty.Println("- RESPONSE HEADER: ")
pretty.Println(resp.Header)
pretty.Println("- RESPONSE BODY: ")
pretty.Println(respReader)
return 0, 0, nil
}
The response status is '200 OK'. Here is the output (Response):
- RESPONSE HEADER:
http.Header{
"Content-Language": {"en"},
"Cache-Control": {"max-age=0"},
"X-Uber-App": {"uberex-nonsandbox", "optimus"},
"Strict-Transport-Security": {"max-age=604800", "max-age=2592000"},
"X-Content-Type-Options": {"nosniff"},
"Date": {"Fri, 19 May 2017 07:52:17 GMT"},
"Content-Geo-System": {"wgs-84"},
"Connection": {"keep-alive"},
"X-Frame-Options": {"SAMEORIGIN"},
"X-Xss-Protection": {"1; mode=block"},
"Server": {"nginx"},
"Content-Type": {"application/json"},
}
- RESPONSE BODY:
&http.gzipReader{
body: &http.bodyEOFSignal{
body: &http.body{
src: &internal.chunkedReader{
r: &bufio.Reader{
buf: {0x48, 0x54, .......... }
I gave in to the stubbornness of the uber api and added another request header, req.Header.Add("Accept-Encoding", "gzip").
Now i am getting the response header "Content-Encoding": "gzip", although i am still getting an undecipherable response body, but that's beyond the scope of this question.
If you don't disable compression [1], and you don't manually request compression with Accept-Encoding: gzip, then what I call "automatic mode" is used. With automatic mode, Go automatically adds Accept-Encoding: gzip, then if server responds Content-Encoding: gzip, Go wrap the response body in a Gzip reader, and removes the Content-Encoding and Content-Length response headers [2]. I disagree with this practice, as the end user is essentially being lied to about what the true response was. Contrast this with cURL, which gives you the pure response, regardless of what you do:
PS C:\> curl -v --compressed https://github.com/manifest.json
< content-encoding: gzip
< content-length: 345
To deal with this, I wrote a wrapper for http.Transport:
package mech
import (
"compress/gzip"
"io"
"net/http"
"strings"
)
type Transport struct { http.Transport }
func (t Transport) RoundTrip(req *http.Request) (*http.Response, error) {
if !t.DisableCompression {
req.Header.Set("Accept-Encoding", "gzip")
}
res, err := t.Transport.RoundTrip(req)
if err != nil {
return nil, err
}
if strings.EqualFold(res.Header.Get("Content-Encoding"), "gzip") {
gz, err := gzip.NewReader(res.Body)
if err != nil {
return nil, err
}
res.Body = readCloser{gz, res.Body}
}
return res, nil
}
type readCloser struct {
io.Reader
io.Closer
}
https://golang.org/pkg/net/http#Transport.DisableCompression
https://github.com/golang/go/blob/go1.16.5/src/net/http/transport.go#L2186-L2192
Related
I have a go code that creates an excel file (using "github.com/tealeg/xlsx") on a daily basis.
The file is then encoded to base64 and sent as an attachment via email, using go-simple-mail.
This works fine 99% of the time, but once in a while the email is sent as a NOOP, with the xlsx file as a string in the email's body.
Here's my code:
func (n *EmailNotifier) verifyServerConnection() error {
err := n.smtpClient.Noop()
if err != nil {
n.smtpClient, err = n.server.Connect()
if err != nil {
return err
}
}
return nil
}
func (n *EmailNotifier) SendFileByEmail(ctx context.Context, addresses []string, file, filename, mailSubject, body string) error {
mailFile := mail.File{
Name: filename,
B64Data: file,
}
err := n.SendEmail(ctx, addresses, mailSubject, body, &mailFile)
if err != nil {
return err
}
return nil
}
func (n *EmailNotifier) SendEmail(ctx context.Context, to []string, mailSubject string, body string, mailFile *mail.File) error {
err := n.verifyServerConnection()
if err != nil {
nl.Log(ctx).WithValues().Error("Error connecting to SMTP server")
return err
}
email := mail.NewMSG()
email.SetFrom(n.server.Username).AddBcc(to...).SetSubject(mailSubject)
email.SetBody(mail.TextPlain, body)
if mailFile != nil {
email.Attach(mailFile)
if email.Error != nil {
nl.Log(ctx).WithError(email.Error).WithValues("attachment error", "noop")
}
}
err = email.Send(n.smtpClient)
if err != nil {
nl.Log(ctx).WithValues("subject", to).WithError(err).Error("Error sending email to")
return err
}
return nil
}
And here is the problematic mail:
NOOP
Subject: xxxxx
Content-Type: multipart/mixed;
boundary=259435eabdfa9a8b5c048944afa23eb1b547d42281bd7269b93eeb559924
Date: Wed, 18 Jan 2023 04:00:14 +0000
Mime-Version: 1.0
From: mailto:xxx#yyy.com
--259435eabdfa9a8b5c048944afa23eb1b547d42281bd7269b93eeb559924
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=UTF-8
--259435eabdfa9a8b5c048944afa23eb1b547d42281bd7269b93eeb559924
Content-Disposition: attachment;
filename="xxx.xlsx"
Content-Transfer-Encoding: base64
Content-Type: application/octet-stream;
name="xxx.xlsx"
UEsDBBQACAAIAAAAAAAAAAAAAAAAAAAAAAARAAAAZG9jUHJvcHMvY29yZS54bWxs0M9KxDAQx/G7
T1Fy30y6gkhpuzdPCoIKXkMydoP5M2RG23172aJVcM/zyRfy6w9Lis0nVg4lD6rVRjWYXfEhT4N6
eb7b3aqGxWZvY8k4qBOyOoxXvaPOlYqPtRBWCcjNkmLmztGgjiLUAbA7YrKsC2FeUnwrNVlhXeoE
ZN27nRD2xtxAQrHeioVzcEdbUX0nvduS9FHjGvAOMGLCLAytbuHXpiAnwosvfo5/tGBNfBGvl00u
HDY1z7Oer1e3N6aF14f7p/Wru5DPUzlUYw//Bhq/AgAA//9QSwcIO+98GtgAAABsAQAAUEsDBBQA....
Decoding this string to an xlsx file generates a valid file. I also tried logging any attachment errors - nothing in the logs.
also - no open issues about this in go-simple-mail
idea-
I am using NOOP to check the server connection, but I am setting the attachment and the recipient only later in my code. Is there any chance that somehow the recipient is set before the NOOP in verifyServerConnection and it messes it up?
Any other suggestions on how to further explore this issue will be welcomed
I'm new to golang and I'm trying to build a small local proxy. The request kinda works from Postman -> localhost:9097 -> localhost:9098 and back. But the content-length is 120 and the response body is just gibberish:
I expect to get a json body like { "result": { "id": "1", "price": 100, "quantity": 1 } }
If a make a request directly to :9098 I see that the response header transfer-encoding is chunked. Any idea how to adjust my code to parse the response body from the server properly and send it back to the client?
func httpHandler(w http.ResponseWriter, req *http.Request) {
reqURL := fmt.Sprint(req.URL)
newUrl = "http://localhost:9098" + reqURL
//forward request
client := http.Client{}
freq, reqerror := http.NewRequest(req.Method, newUrl, nil)
if reqerror != nil {
log.Fatalln(reqerror)
}
freq.Header = req.Header
freq.Body = req.Body
resp, resperr := client.Do(freq)
if resperr != nil {
log.Println(resperr)
fmt.Fprintf(w, "Error. No response")
return
}
defer resp.Body.Close()
body, ioerr := io.ReadAll(resp.Body)
if ioerr != nil {
log.Println(ioerr)
fmt.Fprintf(w, "IO Error (Response body)")
return
}
w.Header().Set("Content-Type", resp.Header.Get("Content-Type"))
w.WriteHeader(resp.StatusCode)
fmt.Fprintf(w, string(body))
}
Managed to solve this now! Thanks to Steffen Ullrich for pointing out the the issue could be "about compressed content".
Removing the Accept-Encoding header as mentioned here worked like a charm.
...
// if you manually set the Accept-Encoding request header, than gzipped response will not automatically decompressed
req.Header.Del("Accept-Encoding")
freq.Header = req.Header
...
Can I print directly to my (physical, external) printer from Golang, without using printer drivers or CUPS or any other such complexity?
Yes! The following prints a postscript file from Golang using IPP (the internet printing protocol), which most network printers manufactured in the past 20 years support:
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"strconv"
"github.com/alexflint/go-arg"
"github.com/kr/pretty"
"github.com/phin1x/go-ipp"
)
func Main() error {
var args struct {
URI string `arg:"positional,required"`
PostscriptFile string `arg:"positional,required"`
}
arg.MustParse(&args)
// define a ipp request
req := ipp.NewRequest(ipp.OperationPrintJob, 1)
req.OperationAttributes[ipp.AttributeCharset] = "utf-8"
req.OperationAttributes[ipp.AttributeNaturalLanguage] = "en"
req.OperationAttributes[ipp.AttributePrinterURI] = args.URI
req.OperationAttributes[ipp.AttributeRequestingUserName] = "some-user"
req.OperationAttributes[ipp.AttributeDocumentFormat] = "application/octet-stream"
// encode request to bytes
payload, err := req.Encode()
if err != nil {
return fmt.Errorf("error encoding ipp request: %w", err)
}
// read the test page
postscript, err := ioutil.ReadFile(args.PostscriptFile)
if err != nil {
return fmt.Errorf("error reading postscript file: %w", err)
}
payload = append(payload, postscript...)
// send ipp request to remote server via http
httpReq, err := http.NewRequest("POST", args.URI, bytes.NewReader(payload))
if err != nil {
return fmt.Errorf("error creating http request: %w", err)
}
// set ipp headers
httpReq.Header.Set("Content-Length", strconv.Itoa(len(payload)))
httpReq.Header.Set("Content-Type", ipp.ContentTypeIPP)
// perform the request
var httpClient http.Client
httpResp, err := httpClient.Do(httpReq)
if err != nil {
return fmt.Errorf("error executing http request: %w", err)
}
defer httpResp.Body.Close()
// read the response
buf, err := io.ReadAll(httpResp.Body)
if err != nil {
return fmt.Errorf("error reading response body: %w", err)
}
// response must be 200 for a successful operation
// other possible http codes are:
// - 500 -> server error
// - 426 -> sever requests a encrypted connection
// - 401 -> forbidden -> need authorization header or user is not permitted
if httpResp.StatusCode != 200 {
return fmt.Errorf("printer said %d: %s", httpResp.StatusCode, buf)
}
// decode ipp response
resp, err := ipp.NewResponseDecoder(bytes.NewReader(buf)).Decode(nil)
if err != nil {
return fmt.Errorf("error decoding ipp response: %w", err)
}
// print the response
fmt.Println("Submitted print job. Response was:")
pretty.Println(resp)
return nil
}
The URL to use is just http://ip-address-of-myprinter (this was tested on a brother HL-series printer)
Does golang's net/http package support requests with chunked transfer-encoding? Thus far I have been able to use the Hijacker interface (https://golang.org/src/net/http/server.go?s=6173:6875#L156) to at least not close the connection and receive the full chunked request, but not yet parsing the chunks and suspect I may be going down the wrong path with this.
From https://golang.org/src/net/http/httputil/httputil.go?s=688:732#L10, I see there is a chunked reader, but appears to be for internal use.
Essentially, I'm trying to accept an HTTP PUT with 'chunked' transfer-encoding and send it off to a backend server 'on-the-fly' (i.e. without buffering the full request in golang). I have no control over the upstream request. Is there a recommended way to handle such a request, or is Hijacker the way to do it?
The net/http client and server transparently read and write chunked bodies.
To accept a chunked request and send it to another HTTP server, pass the server request body as the client request body. Here's now to forward the body to another server as a PUT:
func handler(w http.ResponseWriter, r *http.Request) {
creq, err := http.NewRequest("PUT", url, r.Body)
if err != nil {
// handle error
}
if ct := r.Header.Get("Content-Type"); ct != "" {
creq.Header.Set("Content-Type", ct)
}
cresp, err := http.DefaultClient.Do(creq)
if err != nil {
// handle error
}
... do something with cresp.
}
If you want to copy to a file, then io.Copy the request body to the file.
func handler(w http.ResponseWriter, r *http.Request) {
f, err := os.Create("fname")
if err != nil {
// handle error
}
_, err := io.Copy(f, r.Body)
if err != nil {
// handle error
}
...
}
These snippets copy the body 'on the fly'.
I'm attempting to use Go to write a utility that authenticates and uploads a file by making a multipart http request to our server. Everything seems to go okay, except the file is not arriving on the server. Looking at it further it appears the multipart in the request is empty. Code and request output below. What am I missing in my Go code?
The Code: (I've changed the URL...)
package main
import (
"net/http"
"mime/multipart"
"strings"
"fmt"
"io/ioutil"
"io"
"os"
"bytes"
"flag"
"encoding/json"
)
var (
filename = flag.String("filename", "", "file to upload")
name = flag.String("name", "", "name to give file on server")
username = flag.String("username", "", "username for authentication")
password = flag.String("password", "", "password for authentication")
)
func main() {
flag.Parse()
// Create multipart
var b bytes.Buffer
w := multipart.NewWriter(&b)
f, _ := os.Open(*filename) //open file to send
defer f.Close()
fw, err := w.CreateFormFile("file", *name) //give file a name
if err != nil {
fmt.Println(err)
}
if _, err := io.Copy(fw, f); err != nil { //copy the file to the multipart buffer
fmt.Println(err)
}
w.Close()
// print the head of the multipart data
bs := b.Bytes()
fmt.Printf("%+v\n\n", string(bs[:1000]))
// Send authentication/login
r, e := http.Post("https://mysite/login", "application/json", strings.NewReader(fmt.Sprintf("{\"username\":\"%s\",\"password\":\"%s\"}", *username, *password)))
if e != nil {
fmt.Println(e)
} else {
// Get the token from the body
type Body struct {
Token string
}
// convert json to get the token
body, _ := ioutil.ReadAll(r.Body)
bd := bytes.NewBuffer(body)
dec := json.NewDecoder(bd)
var m Body
dec.Decode(&m)
// Upload file
req, err := http.NewRequest("POST", "https://mysite/api/apps", &b)
if err != nil {
fmt.Printf("%v\n", err)
}
req.Header.Set("Authentication", fmt.Sprintf("Bearer: %s", m.Token))
req.Header.Set("Content-Type", w.FormDataContentType())
client := &http.Client{}
res, err := client.Do(req)
if err != nil {
fmt.Printf("%v\n", err)
}
// print status and request body
fmt.Println(res.Status)
fmt.Printf("%+v\n", res.Request)
}
}
The first thing I print is the byte buffer, b, which contains the multipart data, everything looks good from here. (it was an xml file)
--83451b003d8e5cc38c0e8f60ad318e522cad4818cf293745c84ec36d26d5
Content-Disposition: form-data; name="file"; filename="snapshot-162224-820-99"
Content-Type: application/octet-stream
<manifest>
<projects>
<project name=........
Next I print the status of the request.
200 OK
Then I printed out the request structure, here is where I saw the MultipartForm was empty.
&{Method:GET URL:https://mysite/home/ Proto: ProtoMajor:0 ProtoMinor:0 Header:map[Authentication:[Bearer: DY0LCJL0g] Content-Type:[multipart/form-data; boundary=83451b003d8e5cc38c0e8f60ad318e522cad4818cf293745c84ec36d26d5] Referer:[http://mysite/home/]] Body:<nil> GetBody:<nil> ContentLength:0 TransferEncoding:[] Close:false Host: Form:map[] PostForm:map[] MultipartForm:<nil> Trailer:map[] RemoteAddr: RequestURI: TLS:<nil> Cancel:<nil> Response:0xc42018a360 ctx:<nil>}
I highly doubt if the server really received nothing. The behavior of the printed body having a nil body is expected and documented in http.Response
// Request is the request that was sent to obtain this Response.
// Request's Body is nil (having already been consumed).
// This is only populated for Client requests.
Request *Request
If you want to debug the request body sented, you should either use a mock server or a proxy.
On another note, your code's attempt to login is not going to work. It does not maintain the cookie of login info, so the later requests cannot utilize them.