From d96e3393ec8cb059f9b4cc21be8753b2e24e5864 Mon Sep 17 00:00:00 2001 From: Leonid Bugaev Date: Mon, 17 Aug 2015 20:36:59 +0300 Subject: [PATCH] Handle HTTP error codes --- http_client.go | 33 +++++++++++++++- http_client_test.go | 91 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/http_client.go b/http_client.go index e3d6c1e..6ec1227 100644 --- a/http_client.go +++ b/http_client.go @@ -21,6 +21,7 @@ type HTTPClientConfig struct { FollowRedirects int Debug bool OriginalHost bool + ConnectionTimeout time.Duration Timeout time.Duration ResponseBufferSize int } @@ -69,9 +70,9 @@ func (c *HTTPClient) Connect() (err error) { c.Disconnect() if !strings.Contains(c.host, ":") { - c.conn, err = net.Dial("tcp", c.host+":80") + c.conn, err = net.DialTimeout("tcp", c.host+":80", c.config.ConnectionTimeout) } else { - c.conn, err = net.Dial("tcp", c.host) + c.conn, err = net.DialTimeout("tcp", c.host, c.config.ConnectionTimeout) } if c.scheme == "https" { @@ -124,6 +125,7 @@ func (c *HTTPClient) Send(data []byte) (response []byte, err error) { Debug("[HTTPClient] Connecting:", c.baseURL) if err = c.Connect(); err != nil { log.Println("[HTTPClient] Connection error:", err) + response = errorPayload(HTTP_CONNECTION_ERROR) return } } @@ -142,6 +144,7 @@ func (c *HTTPClient) Send(data []byte) (response []byte, err error) { if _, err = c.conn.Write(data); err != nil { Debug("[HTTPClient] Write error:", err, c.baseURL) + response = errorPayload(HTTP_TIMEOUT) return } @@ -160,6 +163,7 @@ func (c *HTTPClient) Send(data []byte) (response []byte, err error) { if err != nil { Debug("[HTTPClient] Response read error", err, c.conn) + response = errorPayload(HTTP_TIMEOUT) return } @@ -197,3 +201,28 @@ func (c *HTTPClient) Get(path string) (response []byte, err error) { return c.Send([]byte(payload)) } + +const ( + // https://support.cloudflare.com/hc/en-us/articles/200171936-Error-520-Web-server-is-returning-an-unknown-error + HTTP_UNKNOWN_ERROR = "520" + // https://support.cloudflare.com/hc/en-us/articles/200171916-Error-521-Web-server-is-down + HTTP_CONNECTION_ERROR = "521" + // https://support.cloudflare.com/hc/en-us/articles/200171906-Error-522-Connection-timed-out + HTTP_CONNECTION_TIMEOUT = "522" + // https://support.cloudflare.com/hc/en-us/articles/200171946-Error-523-Origin-is-unreachable + HTTP_UNREACHABLE = "523" + // https://support.cloudflare.com/hc/en-us/articles/200171926-Error-524-A-timeout-occurred + HTTP_TIMEOUT = "524" +) + +var errorPayloadTemplate = "HTTP/1.1 202 Accepted\r\nDate: Mon, 17 Aug 2015 14:10:11 GMT\r\nContent-Length: 0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n" + +func errorPayload(errorCode string) []byte { + payload := make([]byte, len(errorPayloadTemplate)) + copy(payload, errorPayloadTemplate) + + copy(payload[29:58], []byte(time.Now().Format(time.RFC1123))) + copy(payload[9:12], errorCode) + + return payload +} diff --git a/http_client_test.go b/http_client_test.go index 6109a90..3f13be2 100644 --- a/http_client_test.go +++ b/http_client_test.go @@ -8,7 +8,11 @@ import ( "net/http" "net/http/httptest" "net/http/httputil" + "github.com/buger/gor/proto" "sync" + "time" + "log" + _ "reflect" "testing" _ "time" ) @@ -299,3 +303,90 @@ func TestHTTPClientHandleHTTP10(t *testing.T) { wg.Wait() } + +func TestHTTPClientErrors(t *testing.T) { + req := []byte("GET http://foobar.com/path HTTP/1.0\r\n\r\n") + + // Port not exists + client := NewHTTPClient("http://127.0.0.1:1", &HTTPClientConfig{Debug: true}) + if resp, err := client.Send(req); err != nil { + if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) { + t.Error("Should return status 521 for connection refused, instead:", string(s)) + } + } else { + t.Error("Should throw error") + } + + client = NewHTTPClient("http://not.existing", &HTTPClientConfig{Debug: true}) + if resp, err := client.Send(req); err != nil { + if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) { + t.Error("Should return status 521 for no such host, instead:", string(s)) + } + } else { + t.Error("Should throw error") + } + + // Non routable IP address to simulate connection timeout + client = NewHTTPClient("http://10.255.255.1", &HTTPClientConfig{Debug: true, ConnectionTimeout: 100 * time.Millisecond }) + + if resp, err := client.Send(req); err != nil { + if s := proto.Status(resp); !bytes.Equal(s, []byte("521")) { + t.Error("Should return status 521 for io/timeout:", string(s)) + } + } else { + t.Error("Should throw error") + } + + // Connecting but io timeout on read + ln, _ := net.Listen("tcp", ":0") + client = NewHTTPClient("http://" + ln.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond}) + + if resp, err := client.Send(req); err != nil { + if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) { + t.Error("Should return status 524 for io read, instead:", string(s)) + } + } else { + t.Error("Should throw error") + } + + // Response read error read tcp [::1]:51128: connection reset by peer &{{0xc20802a000}} + ln1, _ := net.Listen("tcp", ":0") + go func(){ + ln1.Accept() + }() + + client = NewHTTPClient("http://" + ln1.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond}) + + if resp, err := client.Send(req); err != nil { + if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) { + t.Error("Should return status 524 for connection reset by peer, instead:", string(s)) + } + } else { + t.Error("Should throw error") + } + + ln2, _ := net.Listen("tcp", ":0") + go func(){ + for { + buf := make([]byte, 64*1024) + conn, err := ln2.Accept() + + if err != nil { + log.Println("Error while Accept()", err) + continue + } + + conn.Read(buf) + } + }() + + client = NewHTTPClient("http://" + ln2.Addr().String(), &HTTPClientConfig{Debug: true, Timeout: 10 * time.Millisecond}) + + if resp, err := client.Send(req); err != nil { + if s := proto.Status(resp); !bytes.Equal(s, []byte("524")) { + t.Error("Should return status 524 for connection reset by peer, instead:", string(s)) + } + } else { + t.Error("Should throw error") + } +}