NATS async reply to request is not asynchronnous - asynchronous

I am trying to implement request/response functinonality in gnatsd using GO language and I realized that gnatsd does not reply to request in async manner.
I started my investigation using NATS github examples https://github.com/nats-io/go-nats/tree/master/examples - examples nats-req.go and nats-rply.go. The examples works well.
Then I modified them simply to test parallel requests on gnatsd and also to provide some debug info in which goroutine ID the async reply is processed.
There is source code of modified examples.
nats-rply.go has been modified to simply return back text of incoming request with information on current goroutine ID. I have also add to the async processing function 1 second sleep to simulate some processing time.
package main
import (
"fmt"
"github.com/nats-io/go-nats"
"flag"
"log"
"runtime"
"time"
"bytes"
"strconv"
)
// NOTE: Use tls scheme for TLS, e.g. nats-rply -s tls://demo.nats.io:4443 foo hello
func usage() {
log.Fatalf("Usage: nats-rply [-s server][-t] <subject> \n")
}
func printMsg(m *nats.Msg, i int) {
log.Printf("[#%d] Received on [%s]: '%s'\n", i, m.Subject, string(m.Data))
}
func main() {
log.Printf("Main goroutine ID:%d\n", getGID())
var urls = flag.String("s", nats.DefaultURL, "The nats server URLs (separated by comma)")
var showTime = flag.Bool("t", false, "Display timestamps")
//log.SetFlags(0)
flag.Usage = usage
flag.Parse()
args := flag.Args()
if len(args) < 1 {
usage()
}
nc, err := nats.Connect(*urls)
if err != nil {
log.Fatalf("Can't connect: %v\n", err)
}
subj, i := args[0], 0
nc.Subscribe(subj, func(msg *nats.Msg) {
i++
printMsg(msg, i)
//simulation of some processing time
time.Sleep(1 * time.Second)
newreply := []byte(fmt.Sprintf("REPLY TO request \"%s\", GoroutineId:%d", string(msg.Data), getGID()))
nc.Publish(msg.Reply, []byte(newreply))
})
nc.Flush()
if err := nc.LastError(); err != nil {
log.Fatal(err)
}
log.Printf("Listening on [%s]\n", subj)
if *showTime {
log.SetFlags(log.LstdFlags)
}
runtime.Goexit()
}
func getGID() uint64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
b = b[:bytes.IndexByte(b, ' ')]
n, _ := strconv.ParseUint(string(b), 10, 64)
return n
}
nats-req.go has been modified to send 10 requests in separate 10 goroutines started in parallel, the request timeout has been set to 3,5 seconds. I tried goroutines with shared NATS connection (function oneReq()) and also goroutines with its own NATS connections (function onReqSeparateConnect()) - with the same unsuccessful results.
package main
import (
"flag"
"fmt"
"github.com/nats-io/go-nats"
"sync"
"time"
"log"
)
// NOTE: Use tls scheme for TLS, e.g. nats-req -s tls://demo.nats.io:4443 foo hello
func usage() {
log.Fatalf("Usage: nats-req [-s server (%s)] <subject> \n", nats.DefaultURL)
}
func main() {
//var urls = flag.String("s", nats.DefaultURL, "The nats server URLs (separated by comma)")
//log.SetFlags(0)
flag.Usage = usage
flag.Parse()
args := flag.Args()
if len(args) < 1 {
usage()
}
nc, err := nats.Connect(nats.DefaultURL)
if err != nil {
log.Fatalf("Can't connect: %v\n", err)
}
defer nc.Close()
subj := args[0]
var wg sync.WaitGroup
wg.Add(10)
for i := 1; i <= 10; i++ {
//go oneReq(subj, fmt.Sprintf("Request%d", i), nc, &wg)
go oneReqSeparateConnect(subj, fmt.Sprintf("Request%d", i), &wg)
}
wg.Wait()
}
func oneReq(subj string, payload string, nc *nats.Conn, wg *sync.WaitGroup) {
defer wg.Done()
msg, err := nc.Request(subj, []byte(payload), 3500*time.Millisecond)
if err != nil {
if nc.LastError() != nil {
log.Printf("Error in Request: %v\n", nc.LastError())
}
log.Printf("Error in Request: %v\n", err)
} else {
log.Printf("Published [%s] : '%s'\n", subj, payload)
log.Printf("Received [%v] : '%s'\n", msg.Subject, string(msg.Data))
}
}
func oneReqSeparateConnect(subj string, payload string, wg *sync.WaitGroup) {
defer wg.Done()
nc, err := nats.Connect(nats.DefaultURL)
if err != nil {
log.Printf("Can't connect: %v\n", err)
return
}
defer nc.Close()
msg, err := nc.Request(subj, []byte(payload), 3500*time.Millisecond)
if err != nil {
if nc.LastError() != nil {
log.Printf("Error in Request: %v\n", nc.LastError())
}
log.Printf("Error in Request: %v\n", err)
} else {
log.Printf("Published [%s] : '%s'\n", subj, payload)
log.Printf("Received [%v] : '%s'\n", msg.Subject, string(msg.Data))
}
}
And there is result - unwanted behaviour, it looks that nats-rply.go creates only one goroutine for processing incoming reqests and the requests are processed in serial way.
The nats-req.go sends all 10 requests in one time with timeout set to 3,5 seconds. The nats-rply.go starts responding to the request with one second intervals in serial way, so only 3 requests are satisfied until 3,5sec timeout is breached - rest of requests timeouts. The response message also contains GoroutineID which is the same for all incoming requests! Even when nats-req is started again the goroutine id is the same, the ID changes only when nats-rply.go server is restarted.
nats-req.go log
D:\PRAC\TSP\AMON>nats-req foo
2017/08/29 18:46:48 Sending: 'Request9'
2017/08/29 18:46:48 Sending: 'Request7'
2017/08/29 18:46:48 Sending: 'Request10'
2017/08/29 18:46:48 Sending: 'Request4'
2017/08/29 18:46:48 Sending: 'Request8'
2017/08/29 18:46:48 Sending: 'Request6'
2017/08/29 18:46:48 Sending: 'Request1'
2017/08/29 18:46:48 Sending: 'Request5'
2017/08/29 18:46:48 Sending: 'Request2'
2017/08/29 18:46:48 Sending: 'Request3'
2017/08/29 18:46:49 Published [foo] : 'Request9'
2017/08/29 18:46:49 Received [_INBOX.xrsXYOB2QmW1f52pkfLHya.xrsXYOB2QmW1f52pkfLHzJ] : 'REPLY TO request "Request9", GoroutineId:36'
2017/08/29 18:46:50 Published [foo] : 'Request7'
2017/08/29 18:46:50 Received [_INBOX.xrsXYOB2QmW1f52pkfLI02.xrsXYOB2QmW1f52pkfLI0l] : 'REPLY TO request "Request7", GoroutineId:36'
2017/08/29 18:46:51 Published [foo] : 'Request10'
2017/08/29 18:46:51 Received [_INBOX.xrsXYOB2QmW1f52pkfLI1U.xrsXYOB2QmW1f52pkfLI2D] : 'REPLY TO request "Request10", GoroutineId:36'
2017/08/29 18:46:52 Error in Request: nats: timeout
2017/08/29 18:46:52 Error in Request: nats: timeout
2017/08/29 18:46:52 Error in Request: nats: timeout
2017/08/29 18:46:52 Error in Request: nats: timeout
2017/08/29 18:46:52 Error in Request: nats: timeout
2017/08/29 18:46:52 Error in Request: nats: timeout
2017/08/29 18:46:52 Error in Request: nats: timeout
nats-rply.go log
C:\Users\belunek>nats-rply foo
2017/08/29 18:46:46 Main goroutine ID:1
2017/08/29 18:46:46 Listening on [foo]
2017/08/29 18:46:48 [#1] Received on [foo]: 'Request9'
2017/08/29 18:46:49 [#2] Received on [foo]: 'Request7'
2017/08/29 18:46:50 [#3] Received on [foo]: 'Request10'
2017/08/29 18:46:51 [#4] Received on [foo]: 'Request4'
2017/08/29 18:46:52 [#5] Received on [foo]: 'Request8'
2017/08/29 18:46:53 [#6] Received on [foo]: 'Request6'
2017/08/29 18:46:54 [#7] Received on [foo]: 'Request1'
2017/08/29 18:46:55 [#8] Received on [foo]: 'Request5'
2017/08/29 18:46:56 [#9] Received on [foo]: 'Request2'
2017/08/29 18:46:57 [#10] Received on [foo]: 'Request3'
Please any ideas, how to correctly implement request/response communication in NATS with asyns (parallel) response processing?
Thanks for any info.

Gnatsd reply to Request in async manner, but it doesn't start goroutine for each request, just pure async. And because you simulate processing load using time.Sleep, which pauses calling goroutine, it looks like sync processing. If you modify your example to use goroutines, everything works well.
...
nc.Subscribe(subj, func(msg *nats.Msg) {
go handler(msg, i, nc)
})
...
func handler(msg *nats.Msg, i int, nc *nats.Conn) {
i++
printMsg(msg, i)
//simulation of some processing time
time.Sleep(1 * time.Second)
newreply := []byte(fmt.Sprintf("REPLY TO request \"%s\", GoroutineId:%d", string(msg.Data), getGID()))
nc.Publish(msg.Reply, []byte(newreply))
}
Output:
./nats-rply test
2017/08/30 00:17:05 Main goroutine ID:1
2017/08/30 00:17:05 Listening on [test]
2017/08/30 00:17:11 [#1] Received on [test]: 'Request6'
2017/08/30 00:17:11 [#1] Received on [test]: 'Request5'
2017/08/30 00:17:11 [#1] Received on [test]: 'Request1'
2017/08/30 00:17:11 [#1] Received on [test]: 'Request8'
2017/08/30 00:17:11 [#1] Received on [test]: 'Request3'
2017/08/30 00:17:11 [#1] Received on [test]: 'Request7'
2017/08/30 00:17:11 [#1] Received on [test]: 'Request9'
2017/08/30 00:17:11 [#1] Received on [test]: 'Request4'
2017/08/30 00:17:11 [#1] Received on [test]: 'Request2'
2017/08/30 00:17:11 [#1] Received on [test]: 'Request10'
./nats-req test
2017/08/30 00:17:12 Published [test] : 'Request3'
2017/08/30 00:17:12 Received [_INBOX.xoG573m0V7dVoIJxojm6Bq] : 'REPLY TO request "Request3", GoroutineId:37'
2017/08/30 00:17:12 Published [test] : 'Request7'
2017/08/30 00:17:12 Received [_INBOX.xoG573m0V7dVoIJxojm5z6] : 'REPLY TO request "Request7", GoroutineId:42'
2017/08/30 00:17:12 Published [test] : 'Request10'
2017/08/30 00:17:12 Received [_INBOX.xoG573m0V7dVoIJxojm5wY] : 'REPLY TO request "Request10", GoroutineId:43'
2017/08/30 00:17:12 Published [test] : 'Request5'
2017/08/30 00:17:12 Received [_INBOX.xoG573m0V7dVoIJxojm6EO] : 'REPLY TO request "Request5", GoroutineId:34'
2017/08/30 00:17:12 Published [test] : 'Request8'
2017/08/30 00:17:12 Received [_INBOX.xoG573m0V7dVoIJxojm66k] : 'REPLY TO request "Request8", GoroutineId:36'
2017/08/30 00:17:12 Published [test] : 'Request1'
2017/08/30 00:17:12 Received [_INBOX.xoG573m0V7dVoIJxojm64C] : 'REPLY TO request "Request1", GoroutineId:35'
2017/08/30 00:17:12 Published [test] : 'Request2'
2017/08/30 00:17:12 Received [_INBOX.xoG573m0V7dVoIJxojm6Gw] : 'REPLY TO request "Request2", GoroutineId:41'
2017/08/30 00:17:12 Published [test] : 'Request4'
2017/08/30 00:17:12 Received [_INBOX.xoG573m0V7dVoIJxojm69I] : 'REPLY TO request "Request4", GoroutineId:40'
2017/08/30 00:17:12 Published [test] : 'Request9'
2017/08/30 00:17:12 Received [_INBOX.xoG573m0V7dVoIJxojm61e] : 'REPLY TO request "Request9", GoroutineId:39'
2017/08/30 00:17:12 Published [test] : 'Request6'
2017/08/30 00:17:12 Received [_INBOX.xoG573m0V7dVoIJxojm5u0] : 'REPLY TO request "Request6", GoroutineId:38'

Keep in mind that by starting a go-routine from the message handler, your processing order goes out of the window. This is the reason NATS is calling the message handler serially, to give user a guaranteed order. If order is not important to you, then indeed, it is easy to start processing of the message in a separate go-routine (or pool of go-routines).

Related

How to make a HTTP request from server to client using grpc in golang

Problem Statement
I have a client (which dials to the server) and server (that listens for incoming requests) written in golang and with the RPC calls defined. I am trying to initiate an HTTP request on the server side which would in turn execute the RPC call for streaming and send a JSON response back to the user
Challenge
I was able to handle both grpc and HTTP requests on different ports but having issues with passing parameters from the HTTP request onto the RPC call on the server side
Server Code
log.Println("Listening for connections from client ........")
lis, err := net.Listen("tcp", ":9000")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := testApi.Server{}
grpcServer := grpc.NewServer()
testApi.RegisterTestApiServiceServer(grpcServer, &s)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %s", err)
}
func main() {
go runGrpc()
log.Printf("*------ Waiting for requests from users ------*")
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/exchangeId/{test_id}", ConnectAndExchange).Methods("GET")
log.Fatal(http.ListenAndServe(":8080", router))
}
func ConnectAndExchange(w http.ResponseWriter, r *http.Request){
vars := mux.Vars(r)
test_id, _ := strconv.Atoi(vars["test_id"])
log.Println("Test id request from user : ", test_id)
func (s * Server) ConnectAndStream(channelStream TestApiService_ConnectAndStreamServer) error {
// Question: This Id has to come from http request above- test_id
var id int32 = 1234566
// id := a.ConnectAndExchange
log.Println("Id from sam user ", id)
// var id int32 = 1234566
for i := 1; i <= 2; i++ {
id += 1
log.Println("Speed Server is sending data : ", id)
channelStream.Send(&Input{Id: id})
}
for i := 1; i <= 2; i++ {
log.Println("now time to receive")
client_response, err := channelStream.Recv()
log.Println("Response from samd client : ", client_response.Id)
if err != nil {
log.Println("Error while receiving from samd : ", err)
}
}
return nil
}
I am stuck with being able to pass the test_id from the curl request to the RPC call as above. Any input is greatly appreciated
Note
Client - Dials in and connects to the server and starts receiving and sending data (bi-directional streaming)
Both the Http and GRPC client are part of the same server application. So why call the RPC method from the Http handler? The Http handler should have access to the same backend functionality.
Your question is slightly unclear but if you are trying to have your client establish a GRPC connection to the server via the HTTP handler this will not work. The GRPC connection established in this situation is between the server and its self.
Edit - thanks for the clarification. Now I understand better the flow that you are trying to achieve. Your http handler method can make the outgoing grpc call to the server and return the response back via the http.ResponseWriter
For simplicity I have used the hello world example on https://github.com/grpc/grpc-go/tree/master/examples/helloworld
Running the code sample below and hitting http://localhost:1000/exchangeId/Test will show the output
Starting
*------ Waiting for http requests from users on port 1000 ------*
server listening at 127.0.0.1:1001
Test id request from user : Test
Server Received: Test
Greeting: Hello Test
Code sample:
import (
"context"
"log"
"net"
"net/http"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
"github.com/gorilla/mux"
)
var (
grpcserver = "localhost:1001"
)
func main() {
log.Print("Starting")
go StartGrpcServer()
log.Printf("*------ Waiting for http requests from users on port 1000 ------*")
router := mux.NewRouter().StrictSlash(true)
router.HandleFunc("/exchangeId/{test_id}", ConnectAndExchange).Methods("GET")
log.Fatal(http.ListenAndServe(":1000", router))
}
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Server Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}
func StartGrpcServer() {
lis, err := net.Listen("tcp", grpcserver)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
func ConnectAndExchange(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
test_id := vars["test_id"]
log.Println("Test id request from user : ", test_id)
// Set up a connection to the server.
conn, err := grpc.Dial(grpcserver, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)
// Contact the server and print out its response.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := c.SayHello(ctx, &pb.HelloRequest{Name: test_id})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", resp.GetMessage())
w.Write([]byte(resp.GetMessage()))
}

Sending http response after hijacking connection

What I try to achieve:
I want to recieve Http request from client then hijack the connection to monitor it on server side (Checking health of connection). Also I want to send Http response on that hijacked connection. In that order: recieve Http request, get request body, hijack connection, return response to client, monitor connection health on server side.
What I've already achieved:
Here is the code of http request handler:
func (c *HandlerCtx) HijackHandler(w http.ResponseWriter, req *http.Request){
// Fetching request body skipped
// Hijacking the connection
h, _ := w.(http.Hijacker)
conn, br, err := h.Hijack()
if err != nil {
fmt.Println(err.Error())
return
}
responseBody := "Http response from hijacked connection"
hr := http.Response{
Status: "200 OK",
StatusCode: 200,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Header: make(http.Header, 0),
Body: ioutil.NopCloser(bytes.NewBufferString(responseBody)),
ContentLength: int64(len(responseBody)),
TransferEncoding: nil,
Close: false,
Uncompressed: false,
Trailer: nil,
Request: req,
TLS: nil,
}
// Writing response
err = hr.Write(br)
if err != nil{
fmt.Println(err.Error())
return
}
// Sending EOF to allow io.ReadAll(resp.Body) without blocking
if v, ok := conn.(interface{ CloseWrite() error }); ok {
err = v.CloseWrite()
if err != nil {
fmt.Println(err.Error())
}
}
// Monitor connection health
}
This is the client code:
func main(){
// Body skipped for testing purposes
resp, err := http.Post("http://127.0.0.1:8085/hello", "application/json", nil)
if err != nil{
fmt.Println(err.Error())
return
}
b, err := io.ReadAll(resp.Body)
if err != nil{
fmt.Println(err.Error())
return
}
}
Now I recieve unexpected EOF from client after calling err = v.CloseWrite() but when I don't CloseWrite the client code stuck on io.ReadAll(resp.Body)
Is there any way to force client to read that http response? Please help me find solution.

net.Conn never receiving EOF

I'm trying to send an HTTP request over a net.Conn TCP connection and read the subsequent response, but I never seem to receive an EOF from the connection when reading. This makes functions like io.Copy or ioutil.ReadAll useless, as they block forever.
Code:
client.go
const requestString = "GET /test HTTP/1.1\r\n" + "Host: 127.0.0.1:8080\r\n\r\n"
func main() {
dialer := net.Dialer{}
conn, err := dialer.Dial("tcp", "127.0.0.1:8080")
if err != nil {
log.Fatalln(err)
}
defer conn.Close()
_, err = conn.Write([]byte(requestString))
if err != nil {
log.Fatalln(err)
}
buf := make([]byte, 1024)
data := make([]byte, 0)
length := 0
for {
n, err := conn.Read(buf)
if err != nil {
if err != io.EOF {
fmt.Printf("Read error: %v\n", err)
}
break
}
data = append(data, buf[:n]...)
length += n
fmt.Printf("Partial read:\n%s\n", string(buf[:n]))
}
fmt.Println("Response:")
fmt.Println(string(data))
}
server.go
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %s", r.URL.Path[1:])
})
http.ListenAndServe(":8080", nil)
}
Output of running client.go with server.go already running:
Partial read:
HTTP/1.1 200 OK
Date: Wed, 25 Nov 2020 04:09:32 GMT
Content-Length: 11
Content-Type: text/plain; charset=utf-8
Hello, test
The first call to Read() returns the expected response, but lacks an EOF. The subsequent call to Read() hangs forever, and I'm not sure how to determine when the connection has finished. If I interrupt the server process, the client connection closes properly and the response is complete.
How can I either a) receive an EOF or b) determine when the response is complete? All examples I've seen online have something close to my code working, so I'm not sure what I'm doing wrong.
Adding Connection: close to the request headers makes sure that the connection... well, closes.

How to view HTTP/HTTPS Interactions with HTTP client

How do you view what exactly is sent over the wire by a HTTP client and how the connection is configured?
go has packages called httputil & httptrace that can facilitate viewing the HTTP lifecycle, as well as what is actually sent over the wire: http-tracing blog post
httptrace go doc
httputil go doc
NOTE httputil.DumpRequestOut is meant for outgoing messages on the client side and httputil.DumpRequest are meant for incoming messages on the server side
NOTE httputil.DumpRequestOut appends the default transport's headers, so if you customize the transport, the changes would not be reflected. See: Why does the HTTP Client Force an Accept-Encoding header
Sample Implementation:
package main
import (
"crypto/tls"
"fmt"
"net/http"
"net/http/httptrace"
"net/http/httputil"
"net/textproto"
"time"
)
func main() {
url := "https://www.google.com"
client := &http.Client{}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return
}
requestDump, err := httputil.DumpRequestOut(req, false)
if err != nil {
fmt.Printf("%s: REQUEST ERR: %s\n", time.Now(), err)
}
fmt.Printf("%s: REQUEST: \n%s\n", time.Now(), string(requestDump))
trace := &httptrace.ClientTrace{
// GetConn is called before a connection is created or
// retrieved from an idle pool. The hostPort is the
// "host:port" of the target or proxy. GetConn is called even
// if there's already an idle cached connection available.
GetConn: func(hostPort string) {
fmt.Printf("Get Conn: hostPort: %s\n", hostPort)
},
// GotConn is called after a successful connection is
// obtained. There is no hook for failure to obtain a
// connection; instead, use the error from
// Transport.RoundTrip.
GotConn: func(connInfo httptrace.GotConnInfo) {
fmt.Printf("Got Conn: connInfo: %+v\n", connInfo)
},
// PutIdleConn is called when the connection is returned to
// the idle pool. If err is nil, the connection was
// successfully returned to the idle pool. If err is non-nil,
// it describes why not. PutIdleConn is not called if
// connection reuse is disabled via Transport.DisableKeepAlives.
// PutIdleConn is called before the caller's Response.Body.Close
// call returns.
// For HTTP/2, this hook is not currently used.
PutIdleConn: func(err error) {
fmt.Printf("PutIdlConn: ERR: %s\n", err)
},
// GotFirstResponseByte is called when the first byte of the response
// headers is available.
GotFirstResponseByte: func() {
fmt.Println("GotFirstResponseByte")
},
// Got100Continue is called if the server replies with a "100
// Continue" response.
Got100Continue: func() {
fmt.Println("Got100Continue")
},
// Got1xxResponse is called for each 1xx informational response header
// returned before the final non-1xx response. Got1xxResponse is called
// for "100 Continue" responses, even if Got100Continue is also defined.
// If it returns an error, the client request is aborted with that error value.
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
fmt.Printf("Got1xxResponse: code: %d header: %+v\n", code, header)
return nil
},
// DNSStart is called when a DNS lookup begins.
DNSStart: func(dnsInfo httptrace.DNSStartInfo) {
fmt.Printf("DNS Start: dnsInfo: %+v\n", dnsInfo)
},
// DNSDone is called when a DNS lookup ends.
DNSDone: func(dnsInfo httptrace.DNSDoneInfo) {
fmt.Printf("DNS Done: dnsInfo: %+v\n", dnsInfo)
},
// ConnectStart is called when a new connection's Dial begins.
// If net.Dialer.DualStack (IPv6 "Happy Eyeballs") support is
// enabled, this may be called multiple times.
ConnectStart: func(network, addr string) {
fmt.Printf("Connect Start: Network Addr: %s %s\n", network, addr)
},
// ConnectDone is called when a new connection's Dial
// completes. The provided err indicates whether the
// connection completedly successfully.
// If net.Dialer.DualStack ("Happy Eyeballs") support is
// enabled, this may be called multiple times.
ConnectDone: func(network, addr string, err error) {
fmt.Printf("Connect Done: Network Addr: %s %s ERR: %s\n", network, addr, err)
},
// TLSHandshakeStart is called when the TLS handshake is started. When
// connecting to an HTTPS site via an HTTP proxy, the handshake happens
// after the CONNECT request is processed by the proxy.
TLSHandshakeStart: func() {
fmt.Println("TLSHandshakeStart")
},
// TLSHandshakeDone is called after the TLS handshake with either the
// successful handshake's connection state, or a non-nil error on handshake
// failure.
TLSHandshakeDone: func(connState tls.ConnectionState, err error) {
fmt.Printf("TLSHandshakeDone: connState: %+v ERR: %s\n", connState, err)
},
// WroteHeaderField is called after the Transport has written
// each request header. At the time of this call the values
// might be buffered and not yet written to the network.
WroteHeaderField: func(key string, value []string) {
fmt.Printf("WroteHeaderField: key: %s val: %s\n", key, value)
},
// WroteHeaders is called after the Transport has written
// all request headers.
WroteHeaders: func() {
fmt.Println("WroteHeaders")
},
// Wait100Continue is called if the Request specified
// "Expect: 100-continue" and the Transport has written the
// request headers but is waiting for "100 Continue" from the
// server before writing the request body.
Wait100Continue: func() {
fmt.Println("Wait100Continue")
},
// WroteRequest is called with the result of writing the
// request and any body. It may be called multiple times
// in the case of retried requests.
WroteRequest: func(info httptrace.WroteRequestInfo) {
fmt.Printf("WroteRequest: %+v\n", info)
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
resp, err := client.Do(req)
fmt.Printf("%s: RESPONSE OBJ: \n%v\n", time.Now(), resp)
}
Output:
2020-07-29 14:09:53.682167 -0700 PDT m=+0.000769969: REQUEST:
GET / HTTP/1.1
Host: www.google.com
User-Agent: Go-http-client/1.1
Accept-Encoding: gzip
Get Conn: hostPort: www.google.com:443
DNS Start: dnsInfo: {Host:www.google.com}
DNS Done: dnsInfo: {Addrs:[{IP:172.217.17.100 Zone:} {IP:2a00:1450:400e:806::2004 Zone:}] Err:<nil> Coalesced:false}
Connect Start: Network Addr: tcp 172.217.17.100:443
Connect Done: Network Addr: tcp 172.217.17.100:443 ERR: %!s(<nil>)
TLSHandshakeStart
TLSHandshakeDone: connState: {Version:772 HandshakeComplete:true DidResume:false CipherSuite:4865 NegotiatedProtocol:h2 NegotiatedProtocolIsMutual:true ServerName: PeerCertificates:[0xc0001d6000 0xc0001d6580] VerifiedChains:[[0xc0001d6000 0xc0001d6580 0xc000278b00]] SignedCertificateTimestamps:[] OCSPResponse:[] ekm:0x1226ae0 TLSUnique:[]} ERR: %!s(<nil>)
Got Conn: connInfo: {Conn:0xc0001a2000 Reused:false WasIdle:false IdleTime:0s}
WroteHeaderField: key: :authority val: [www.google.com]
WroteHeaderField: key: :method val: [GET]
WroteHeaderField: key: :path val: [/]
WroteHeaderField: key: :scheme val: [https]
WroteHeaderField: key: accept-encoding val: [gzip]
WroteHeaderField: key: user-agent val: [Go-http-client/2.0]
WroteHeaders
WroteRequest: {Err:<nil>}
GotFirstResponseByte
2020-07-29 14:09:54.620195 -0700 PDT m=+0.938796345: RESPONSE OBJ:
&{200 OK 200 HTTP/2.0 2 0 map[Alt-Svc:[h3-29=":443"; ma=2592000,h3-27=":443"; ma=2592000,h3-T050=":443"; ma=2592000,h3-Q050=":443"; ma=2592000,h3-Q046=":443"; ma=2592000,h3-Q043=":443"; ma=2592000,quic=":443"; ma=2592000; v="46,43"] Cache-Control:[private, max-age=0] Content-Type:[text/html; charset=ISO-8859-1] Date:[Wed, 29 Jul 2020 21:09:54 GMT] Expires:[-1] P3p:[CP="This is not a P3P policy! See g.co/p3phelp for more info."] Server:[gws] Set-Cookie:[1P_JAR=2020-07-29-21; expires=Fri, 28-Aug-2020 21:09:54 GMT; path=/; domain=.google.com; Secure NID=204=qnJT-6IGam7-C1fTR8uIkbDPnfV7OwgOGn5-6tGCWLYmeaRMoSKgV1qSRfKGLghNgQVWY9N_o6hUWKm69I5KrdVqIEVVxRy6XSY6F4c1JyTJZZqEMxMlkpznu-PWOn9eAezKBONTxCZgsGZYboEeYZ5-qZBjUvd7BratNIPkTxU; expires=Thu, 28-Jan-2021 21:09:54 GMT; path=/; domain=.google.com; HttpOnly] X-Frame-Options:[SAMEORIGIN] X-Xss-Protection:[0]] 0xc00018c1e0 -1 [] false true map[] 0xc000112100 0xc00007c000}

GoLang 1.6 - HTTPS EOF error

I'm currently attempting to debug an issue with issuing a HTTP POST via GoLang 1.6.
The connection is failing with an 'EOF' error.
The test client code: https://gist.github.com/fatmcgav/35d1a4fbd74c7208f445c487f756a5e1
package main
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/http/httputil"
"os"
)
func main() {
const body = "{\"auth\":" +
"{\"identity\":" +
"{\"methods\":[\"password\"]," +
"\"password\":" +
"{\"user\":{\"name\":\"xxx\"," +
"\"password\":\"xxx\"," +
"\"domain\":{\"name\":\"xxx_domain\"}" +
"}" +
"}" +
"},\"scope\":{\"project\":" +
"{\"domain\": " +
"{\"name\":\"xxx_domain\"},\"name\":\"xxx\"}}}}"
// Unmarshall JSON
var m map[string]interface{}
json.Unmarshal([]byte(body), &m)
// fmt.Println("%v", m)
var req *http.Request
var resp *http.Response
var dump []byte
var err error
client := &http.Client{}
url := "https://identity.xxx/v3/auth/tokens"
fmt.Println("Requesting auth token against URL: %s", url)
rendered, err := json.Marshal(m)
if err != nil {
fmt.Println("Error marshalling body: %q", err)
os.Exit(2)
}
req, err = http.NewRequest("POST", url, bytes.NewReader(rendered))
if err != nil {
fmt.Println("Got an error: %q", err)
os.Exit(2)
}
// Need to Close connection
req.Close = true
fmt.Println("Setting Insecure TLS mode")
// Configure custom TLS settings.
config := &tls.Config{InsecureSkipVerify: true}
transport := &http.Transport{TLSClientConfig: config,
DisableKeepAlives: true,
DisableCompression: true,
}
client.Transport = transport
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
dump, err = httputil.DumpRequestOut(req, true)
fmt.Printf("Outgoing requst = \n%q\n", dump)
resp, err = client.Do(req)
if err != nil {
fmt.Println("Error encountered: %q", err)
}
dump, err = httputil.DumpResponse(resp, true)
fmt.Printf("Response = \n%q\n", dump)
}
and resultant output is here:
~/golang/scripts  go build req-test.go && ./req-test
Requesting auth token against URL: %s https://identity.xxx/v3/auth/tokens
Setting Insecure TLS mode
Outgoing requst =
"POST /v3/auth/tokens HTTP/1.1\r\nHost: identity.xxx\r\nUser-Agent: Go-http-client/1.1\r\nConnection: close\r\nContent-Length: 262\r\nAccept: application/json\r\nContent-Type: application/json\r\nAccept-Encoding: gzip\r\n\r\n{\"auth\":{\"identity\":{\"methods\":[\"password\"],\"password\":{\"user\":{\"domain\":{\"name\":\"BusinessSupport_domain\"},\"name\":\"xxx\",\"password\":\"xxx\"}}},\"scope\":{\"project\":{\"domain\":{\"name\":\"xxx_domain\"},\"name\":\"xxx\"}}}}"
Error encountered: %q Post https://identity.xxx/v3/auth/tokens: EOF
panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x0 pc=0x13659e]
goroutine 1 [running]:
panic(0x338980, 0xc82000a170)
/usr/local/opt/go/libexec/src/runtime/panic.go:464 +0x3e6
net/http/httputil.DumpResponse(0x0, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0)
/usr/local/opt/go/libexec/src/net/http/httputil/dump.go:285 +0x42e
main.main()
/Users/gavinw/golang/scripts/req-test.go:77 +0xdc
For obvious reasons, I've anonymised some of the details..
Any pointers on what I'm doing wrong..
Cheers
Gavin

Resources