Per this doc, we can trace http.Client with httptrace in this way
t := &transport{}
req, _ := http.NewRequest("GET", "https://google.com", nil)
trace := &httptrace.ClientTrace{
GotConn: t.GotConn,
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
client := &http.Client{Transport: t}
For google API client, here are the one wrapper codes
func NewWithClient(jsonKey []byte, cli *http.Client) (*Client, error) {
if cli == nil {
return nil, fmt.Errorf("client is nil")
}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cli)
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope)
if err != nil {
return nil, err
}
service, err := androidpublisher.NewService(ctx, option.WithHTTPClient(conf.Client(ctx)))
if err != nil {
return nil, err
}
return &Client{service}, err
}
We want to apply httptrace to the http.Client argument of NewWithClient to do HTTP trace.
What we have tried
type TraceTransport struct {
}
var traceTransport = &TraceTransport{}
var trace = &httptrace.ClientTrace{
GotConn: traceTransport.GotConn,
}
func (t *TraceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
return http.DefaultTransport.RoundTrip(req)
}
func (t *TraceTransport) GotConn(info httptrace.GotConnInfo) {
fmt.Printf("Connection reused for %v \n", info.Reused)
}
type ClientWrapper struct {
defaultClient *http.Client
}
var clientWrapperTrace = &httptrace.ClientTrace{GotConn: traceTransport.GotConn}
func (c *ClientWrapper) Do(req *http.Request) (*http.Response, error) {
req = req.WithContext(httptrace.WithClientTrace(req.Context(), clientWrapperTrace))
return c.defaultClient.Do(req)
}
func NewClientTrace(jsonKey []byte) (*Client, error) {
cli := &http.Client{
Transport: traceTransport,
Timeout: time.Duration(10) * time.Second,
}
cliWrapper := &ClientWrapper{defaultClient: cli}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cliWrapper)
conf, err := google.JWTConfigFromJSON(jsonKey, androidpublisher.AndroidpublisherScope)
if err != nil {
return nil, err
}
service, err := androidpublisher.NewService(ctx, option.WithHTTPClient(conf.Client(ctx)))
if err != nil {
return nil, err
}
return &Client{service}, err
}
type Client struct {
service *androidpublisher.Service
}
func (c *Client) VerifyProduct(
ctx context.Context,
packageName string,
productID string,
token string,
) (*androidpublisher.ProductPurchase, error) {
ps := androidpublisher.NewPurchasesProductsService(c.service)
result, err := ps.Get(packageName, productID, token).Context(ctx).Do()
return result, err
}
// test codes
c, err := NewClientTrace([]byte(privateKey))
if err != nil {
return
}
packageName := "package.name"
productID := "product_id"
token := "xxxxx"
r, err := c.VerifyProduct(context.Background(), packageName, productID, token)
However, it is failed to trace http.Client, There is no output of GotConn. Could someone help us to figure out the issue of the above codes?
Requests from google/oauth2 are not traceable by httptrace. your ClientWrapper passed with context.WithValue will be ignored here, and oauth2 has it's own http.Client, it just use the Transport method of *http.Client from context.Value.
Requests from androidpublisher can be traced by httptrace like this:
ctx := httptrace.WithClientTrace(context.Background(), clientWrapperTrace)
r, err := c.VerifyProduct(ctx, packageName, productID, token)
If you just want to count the requests, i think overwrite the http.Client.Transport is a easy way.
type TraceTransport struct {
}
func (t *TraceTransport) RoundTrip(req *http.Request) (*http.Response, error) {
fmt.Printf("RoundTrip hook %v\n", req.URL)
return http.DefaultTransport.RoundTrip(req)
}
func NewClientTrace(jsonKey []byte) (*Client, error) {
cli := &http.Client{Transport: &TraceTransport{}}
ctx := context.WithValue(context.Background(), oauth2.HTTPClient, cli)
// ...
service, err := androidpublisher.NewService(ctx, option.WithHTTPClient(conf.Client(ctx)))
// ....
}
Related
I am having issue in keeping websocket connection alive in go. In my code below, I assign 2 different ports to handle websocket (:8080) and for API request (:3300).
There is no issue when I am using websocket handler directly, but using API handler request and making new external request to the websocker handler, the connection closed directly. Any help is appreciated.
package main
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"time"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
)
func main() {
go websocket()
http.HandleFunc("/ws", func(rw http.ResponseWriter, r *http.Request) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
return
}
url := fmt.Sprintf("http://127.0.0.1:8080?%s", r.URL.RawQuery)
req, err := http.NewRequest(r.Method, url, bytes.NewReader(body))
if err != nil {
fmt.Println(err)
panic(err)
}
req.Header = make(http.Header)
for h, val := range r.Header {
req.Header[h] = val
}
httpClient := &http.Client{Timeout: time.Second * 10}
httpClient.Do(req)
})
http.ListenAndServe(":3300", nil)
}
func websocket() {
http.ListenAndServe(":8080", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
fmt.Println(err)
return
}
go func() {
defer conn.Close()
for {
msg, op, err := wsutil.ReadClientData(conn)
if err != nil {
fmt.Println(err)
return
}
err = wsutil.WriteServerMessage(conn, op, msg)
if err != nil {
fmt.Println(err)
return
}
}
}()
}))
}
The code in the question connects to the websocket endpoint using an HTTP request. Upgrade fails as a result.
Use the standard library reverse proxy to proxy the request.
A simpler approach is to is to call the websocket handler directly. Move the handler to a top-level function:
func handleWS(w http.ResponseWriter, r *http.Request) {
conn, _, _, err := ws.UpgradeHTTP(r, w)
if err != nil {
fmt.Println(err)
return
}
go func() {
defer conn.Close()
for {
msg, op, err := wsutil.ReadClientData(conn)
if err != nil {
fmt.Println(err)
return
}
err = wsutil.WriteServerMessage(conn, op, msg)
if err != nil {
fmt.Println(err)
return
}
}
}()
}
Use the handler in both servers.
func main() {
go websocket()
http.HandleFunc("/ws", handleWS)
http.ListenAndServe(":3300", nil)
}
func websocket() {
http.ListenAndServe(":8080", http.HandlerFunc(handleWS))
}
I have a handler which I call from my main() function:
type requestBody struct {
Query string `json:"query"`
}
func main() {
r := chi.NewRouter()
r.Post("/api", MyHandler(superGraph, gqlGen))
}
func MyHandler(library *MyLibrary, next http.Handler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
buf := bytes.NewBuffer(make([]byte, 0))
reader := io.TeeReader(r.Body, buf)
var reqBody requestBody
err := json.NewDecoder(reader).Decode(&reqBody)
if err != nil {
http.Error(w, "cannot read body", http.StatusBadRequest)
return
}
res, err := library.DoSomething(...)
if err != nil {
log.Error(err)
err := r.Body.Close()
log.ErrorIf(err)
r.Body = ioutil.NopCloser(buf)
next.ServeHTTP(w, r)
return
}
render.JSON(w, r, res) // go-chi "render" pkg
}
}
QUESTION
Do I need the below line?
err := r.Body.Close()
I know https://stackoverflow.com/a/42533540/10088259:
A request body does not need to be closed in the handler. From the http.Request documentation:
The Server will close the request body. The ServeHTTP
Handler does not need to.
but here I'm using:
reader := io.TeeReader(r.Body, buf)
and if err != nil {
r.Body = ioutil.NopCloser(buf)
So, should I r.Body.Close() it in the if path of my code considering that ioutil.NopCloser() has a "fake" Close() method?
I need a golang client that can upgrade from an http get response to a websocket connection.
I have a JS client that works and I've seen direct ws client connections but I have to upgrade from http. I have tried looking for other 3GL solutions (Java, C#, Python) but I need to be able to implement the upgrade in Go. I have seen Dart detaching the socket and creating a websocket from it.
WebSocket.fromUpgradedSocket
I noticed Client does not support Hijack but the discussion didn't get me anywhere.
I am using github.com/gorilla/websocket but can change that if it helps.
Server:
func main() {
srv := Srv{}
count = 0
http.HandleFunc("/", srv.handleRoot)
http.HandleFunc("/ws", srv.handleWs)
log.Fatal(http.ListenAndServe(":5002", nil))
}
func (tool *Srv) handleRoot(w http.ResponseWriter, r *http.Request) {
webSocketKey := hdr.Get("Sec-WebSocket-Key")
log.Printf("Socket key = '%v'", webSocketKey)
secWsAccept := computeAcceptKey(webSocketKey)
log.Printf("Accept = '%v'", secWsAccept)
w.Header().Add("sec-websocket-accept", secWsAccept)
w.Header().Add("upgrade", "websockt")
w.Header().Add("connection", "upgrade")
w.WriteHeader(101)
}
func (tool *Srv) handleWs(w http.ResponseWriter, r *http.Request) {
var upgrader = websocket.Upgrader{}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatalf("Websocket fatal error. %v", err)
}
tool.conn = conn
go tool.serviceWsRequests()
}
func (tool *Srv) serviceWsRequests() {
for {
log.Printf("starting ws")
req := request{}
err := tool.conn.ReadJSON(&req)
if err != nil {
log.Printf("Failed to decode ws message. %v", err)
break
}
fmt.Printf("Got request. %v\n", req)
if req.Method == "ping" {
fmt.Printf("Param=%v\n", req.Parameters)
}
}
}
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
func computeAcceptKey(challengeKey string) string {
h := sha1.New()
h.Write([]byte(challengeKey))
h.Write(keyGUID)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
Client:
func main() {
tr := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
DisableCompression: true,
}
client := &http.Client{
Transport: tr,
// Do NOT follow redirects
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
wsKey, err := generateKey()
if err != nil {
log.Printf("Cannot generate challenge key %v", err)
}
// Get request for ws upgrade.
req, err := http.NewRequest("GET", "http://localhost:5002", nil)
req.Header.Add("Connection", "Upgrade")
req.Header.Add("Upgrade", "websocket")
req.Header.Add("Sec-WebSocket-Version", "13")
req.Header.Add("Sec-WebSocket-Key", wsKey)
log.Printf("ws key '%v'", wsKey)
resp, err := client.Do(req)
if err != nil {
log.Printf("Get error %v", err)
}
defer func() {
if resp != nil {
err = resp.Body.Close()
}
}()
log.Printf("Status='%v', proto='%v'", resp.Status, resp.Proto)
body, err := ioutil.ReadAll(resp.Body)
hdr := resp.Header
for k, v := range hdr{
log.Printf("%v : %v", k, v)
}
log.Printf("Body = %v", string(body))
resp, err = http.Get("ws://localhost:5002/ws")
if err != nil {
log.Printf("Error '%v'", err)
}
}
func generateKey() (string, error) {
p := make([]byte, 16)
if _, err := io.ReadFull(rand.Reader, p); err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(p), nil
}
var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
func computeAcceptKey(challengeKey string) string {
h := sha1.New()
h.Write([]byte(challengeKey))
h.Write(keyGUID)
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
I get an error
Error 'Get ws://localhost:5002/ws: unsupported protocol scheme "ws"'
Which doesn't surprise me because I haven't upgraded the connection.
So how do I go an upgrade in Go?
Use the Gorilla client to dial websocket connections:
func main() {
c, _ , err := websocket.DefaultDialer.Dial("ws://localhost:5002/ws", nil)
if err != nil {
// handle error
}
defer c.Close()
// do something with c, a *websocket.Conn
}
The Dial method issues a GET to the server requesting an upgrade to the WebSocket protocol. On successful completion of the upgrade, Dial returns a *websocket.Conn.
I'm working on some tests in Go and I have spent the past 2 days trying to make it work but I couldn't. My problem is that the test returns 400 even when the user does exist.
This is my getUser function
func (handler *UserHandler) getUser(w http.ResponseWriter, ID int) {
logfile, err := os.OpenFile("events.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("Error opening file: %v", err)
}
defer logfile.Close()
log.SetOutput(logfile)
user := db.Fetch(ID)
userJSON, err := json.Marshal(user)
if err != nil {
log.Printf("Error while marshaling the user into JSON: %v", err)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// userJSON is sent as http Response
w.Write(userJSON)
}
This is my UserHandler
type UserHandler struct{}
func (handle *UserHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = ShiftPath(r.URL.Path)
id, err := strconv.Atoi(head)
if err != nil {
http.Error(w, fmt.Sprintf("Invalid user ID %q", head), http.StatusBadRequest)
return
}
switch r.Method {
case "GET":
handle.getUser(w, id)
default:
http.Error(w, "Only GET is allowed", http.StatusMethodNotAllowed)
}
}
func ShiftPath(p string) (head, tail string) {
p = path.Clean("/" + p)
i := strings.Index(p[1:], "/") + 1
if i <= 0 {
return p[1:], "/"
}
return p[1:i], p[i:]
}
And this is my test
func TestGetUser(t *testing.T) {
handler := new(UserHandler)
mux := http.NewServeMux()
mux.HandleFunc("/user/", handler.ServeHTTP)
writer := httptest.NewRecorder()
request, _ := http.NewRequest("GET", "/user/12", nil)
mux.ServeHTTP(writer, request)
if writer.Code != 200 {
t.Errorf("Response code is %v", writer.Code)
}
}
Issue with code ====> id, err := strconv.Atoi(head)
Due to error you see a return and hence you see 400 error.
Have your server code fully functional with valid logic.
Suggestion: Always print or debug line by line. You can find the issue and root cause.
How do I POST to an API with Content-Type: multipart/form-data, []byte parameters and string arguments? I have tried, but it is failing.
Error message:
details: "[301 301 Moved Permanently]<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">\r\n<html>\r\n301 Moved Permanently\r\n<body bgcolor=\"white\">\r\n301 Moved Permanently\r\n<p>The requested resource has been assigned a new permanent URI.</p >\r\n<hr/>Powered by Tengine/2.1.0</body>\r\n</html>\r\n"
Go code:
func NewPost2(url string) ([]byte, error) {
m := make(map[string]interface{}, 0)
m["fileName"] ="good"
m["name"] = Base64ToByte("/9j/4AAQSkZJRgABAQEAeAB4AAD/2wBDAAIBAQIBAQICAgICAgICAwUDAwMDAwYEBAMFBwYHBwcGBwcICQsJCAgKCAcHCg0KCgsMDAwMBwkODw0MDgsMDAz/2wBDAQICAgMDAwYDAwYMCAcIDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAz/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDHooor+wD+Zz//2Q==")
b, _ := json.Marshal(m)
httpReq, err := http.NewRequest("POST", url, bytes.NewBuffer(b))
httpReq.Header.Set("Content-Type", "multipart/form-data;charset=UTF-8")
client := &http.Client{}
resp, err := client.Do(httpReq)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("[%d %s]%s", resp.StatusCode, resp.Status, string(b))
}
respData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return respData, nil
}
Now, I am very happy with the mood to share my solution
func NewPostFile(url string, paramTexts map[string]interface{}, paramFile FileItem) ([]byte, error) {
// if paramFiles ==nil {
// return NewPost(url,paramTexts,header,transport)
// }
bodyBuf := &bytes.Buffer{}
bodyWriter := multipart.NewWriter(bodyBuf)
for k, v := range paramTexts {
bodyWriter.WriteField(k, v.(string))
}
fileWriter, err := bodyWriter.CreateFormFile(paramFile.Key, paramFile.FileName)
if err != nil {
fmt.Println(err)
//fmt.Println("Create form file error: ", error)
return nil, err
}
fileWriter.Write(paramFile.Content)
contentType := bodyWriter.FormDataContentType()
bodyWriter.Close()
fmt.Println(bodyBuf.String())
resp, err := http.Post(url, contentType, bodyBuf)
if err != nil {
return nil, err
}
defer resp.Body.Close()
fmt.Println(resp)
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
b, _ := ioutil.ReadAll(resp.Body)
return nil, fmt.Errorf("[%d %s]%s", resp.StatusCode, resp.Status, string(b))
}
respData, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
fmt.Println(string(respData))
return respData, nil
}
type FileItem struct {
Key string //image_content
FileName string //test.jpg
Content []byte //[]byte
}
I wrapped the multipart code in a function, as you need to Close it before you
can make a request. Also my method is using # as a heuristic, similar to
cURL [1]:
package main
import (
"bytes"
"io"
"mime/multipart"
"os"
"strings"
)
func createForm(form map[string]string) (string, io.Reader, error) {
body := new(bytes.Buffer)
mp := multipart.NewWriter(body)
defer mp.Close()
for key, val := range form {
if strings.HasPrefix(val, "#") {
val = val[1:]
file, err := os.Open(val)
if err != nil { return "", nil, err }
defer file.Close()
part, err := mp.CreateFormFile(key, val)
if err != nil { return "", nil, err }
io.Copy(part, file)
} else {
mp.WriteField(key, val)
}
}
return mp.FormDataContentType(), body, nil
}
Example:
package main
import "net/http"
func main() {
form := map[string]string{"profile": "#portrait.jpg", "name": "John"}
ct, body, err := createForm(form)
if err != nil {
panic(err)
}
http.Post("https://stackoverflow.com", ct, body)
}
https://curl.se/docs/manpage.html#-F
On a 301 response, the new url is specified in the headers of the response, not its body
(see https://en.wikipedia.org/wiki/HTTP_301)
try printing :
resp.Header["Location"]
If you have this error as a final response, this also means that the http.Client chose to not follow this redirection.
The doc says that the dafult policy for a Client is to follow up to 10 redirects.
In order to debug redirects, you can write your own CheckRedirect function, which can for instance print the sequence of urls