Repository: labstack/echo
Pull request #1197 (2018-09-23 18:42:45)

Proxy: Better errors + remote custom TLS
1. Added better logging on connection issues to remote hosts.
2. Make transport customization to enable (for example) custom remote TLS certificates.

codecov[bot]: # [Codecov](https://codecov.io/gh/labstack/echo/pull/1197?src=pr&el=h1) Report
> Merging [#1197](https://codecov.io/gh/labstack/echo/pull/1197?src=pr&el=desc) into [master](https://codecov.io/gh/labstack/echo/commit/a2d4cb9c7a629e2ee21861501690741d2374de10?src=pr&el=desc) will **increase** coverage by `0.1%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/labstack/echo/pull/1197/graphs/tree.svg?width=650&token=vqnT76l79i&height=150&src=pr)](https://codecov.io/gh/labstack/echo/pull/1197?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master    #1197     +/-   ##
=========================================
+ Coverage   81.54%   81.65%   +0.1%     
=========================================
  Files          25       25             
  Lines        1913     1924     +11     
=========================================
+ Hits         1560     1571     +11     
  Misses        247      247             
  Partials      106      106
```


| [Impacted Files](https://codecov.io/gh/labstack/echo/pull/1197?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [middleware/proxy.go](https://codecov.io/gh/labstack/echo/pull/1197/diff?src=pr&el=tree#diff-bWlkZGxld2FyZS9wcm94eS5nbw==) | `67.85% <100%> (+3.5%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/labstack/echo/pull/1197?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/labstack/echo/pull/1197?src=pr&el=footer). Last update [a2d4cb9...3f56382](https://codecov.io/gh/labstack/echo/pull/1197?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/echo.go b/echo.go
index 2b3b8eb..3c03659 100644
--- a/echo.go
+++ b/echo.go
@@ -260,6 +260,7 @@ var (
 	ErrBadGateway                  = NewHTTPError(http.StatusBadGateway)
 	ErrInternalServerError         = NewHTTPError(http.StatusInternalServerError)
 	ErrRequestTimeout              = NewHTTPError(http.StatusRequestTimeout)
+	ErrServiceUnavailable          = NewHTTPError(http.StatusServiceUnavailable)
 	ErrValidatorNotRegistered      = errors.New("validator not registered")
 	ErrRendererNotRegistered       = errors.New("renderer not registered")
 	ErrInvalidRedirectCode         = errors.New("invalid redirect status code")
diff --git a/middleware/proxy.go b/middleware/proxy.go
index 2686c30..1789c75 100644
--- a/middleware/proxy.go
+++ b/middleware/proxy.go
@@ -6,7 +6,6 @@ import (
 	"math/rand"
 	"net"
 	"net/http"
-	"net/http/httputil"
 	"net/url"
 	"regexp"
 	"strings"
@@ -38,10 +37,14 @@ type (
 		// "/users/*/orders/*": "/user/$1/order/$2",
 		Rewrite map[string]string
 
-		// Context key to store selected ProxyTarget into context.
+    // Context key to store selected ProxyTarget into context.
 		// Optional. Default value "target".
 		ContextKey string
 
+    // To customize the transport to remote.
+		// Examples: If custom TLS certificates are required.
+		Transport http.RoundTripper
+
 		rewriteRegex map[*regexp.Regexp]string
 	}
 
@@ -85,10 +88,6 @@ var (
 	}
 )
 
-func proxyHTTP(t *ProxyTarget) http.Handler {
-	return httputil.NewSingleHostReverseProxy(t.URL)
-}
-
 func proxyRaw(t *ProxyTarget, c echo.Context) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		in, _, err := c.Response().Hijack()
@@ -250,7 +249,7 @@ func ProxyWithConfig(config ProxyConfig) echo.MiddlewareFunc {
 				proxyRaw(tgt, c).ServeHTTP(res, req)
 			case req.Header.Get(echo.HeaderAccept) == "text/event-stream":
 			default:
-				proxyHTTP(tgt).ServeHTTP(res, req)
+				proxyHTTP(tgt, c, config).ServeHTTP(res, req)
 			}
 
 			return
diff --git a/middleware/proxy_1_11.go b/middleware/proxy_1_11.go
new file mode 100644
index 0000000..db56629
--- /dev/null
+++ b/middleware/proxy_1_11.go
@@ -0,0 +1,24 @@
+// +build go1.11
+
+package middleware
+
+import (
+	"fmt"
+	"github.com/labstack/echo"
+	"net/http"
+	"net/http/httputil"
+)
+
+func proxyHTTP(tgt *ProxyTarget, c echo.Context, config ProxyConfig) http.Handler {
+	proxy := httputil.NewSingleHostReverseProxy(tgt.URL)
+	proxy.ErrorHandler = func(resp http.ResponseWriter, req *http.Request, err error) {
+		descr := tgt.URL.String()
+		if tgt.Name != "" {
+			descr = fmt.Sprintf("%s(%s)", tgt.Name, tgt.URL.String())
+		}
+		c.Logger().Errorf("remote %s unreachable, could not forward: %v", descr, err)
+		c.Error(echo.ErrServiceUnavailable)
+	}
+	proxy.Transport = config.Transport
+	return proxy
+}
diff --git a/middleware/proxy_1_11_n.go b/middleware/proxy_1_11_n.go
new file mode 100644
index 0000000..d4620e2
--- /dev/null
+++ b/middleware/proxy_1_11_n.go
@@ -0,0 +1,13 @@
+// +build !go1.11
+
+package middleware
+
+import (
+	"github.com/labstack/echo"
+	"net/http"
+	"net/http/httputil"
+)
+
+func proxyHTTP(t *ProxyTarget, c echo.Context, config ProxyConfig) http.Handler {
+	return httputil.NewSingleHostReverseProxy(t.URL)
+}


Test patch:
diff --git a/middleware/proxy_1_11_test.go b/middleware/proxy_1_11_test.go
new file mode 100644
index 0000000..19bbc26
--- /dev/null
+++ b/middleware/proxy_1_11_test.go
@@ -0,0 +1,52 @@
+// +build go1.11
+
+package middleware
+
+import (
+	"github.com/labstack/echo"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+)
+
+func TestProxy_1_11(t *testing.T) {
+	// Setup
+	url1, _ := url.Parse("http://127.0.0.1:27121")
+	url2, _ := url.Parse("http://127.0.0.1:27122")
+
+	targets := []*ProxyTarget{
+		{
+			Name: "target 1",
+			URL:  url1,
+		},
+		{
+			Name: "target 2",
+			URL:  url2,
+		},
+	}
+	rb := NewRandomBalancer(nil)
+	// must add targets:
+	for _, target := range targets {
+		assert.True(t, rb.AddTarget(target))
+	}
+
+	// must ignore duplicates:
+	for _, target := range targets {
+		assert.False(t, rb.AddTarget(target))
+	}
+
+	// Random
+	e := echo.New()
+	e.Use(Proxy(rb))
+	req := httptest.NewRequest(echo.GET, "/", nil)
+	rec := newCloseNotifyRecorder()
+
+	// Remote unreachable
+	rec = newCloseNotifyRecorder()
+	req.URL.Path = "/api/users"
+	e.ServeHTTP(rec, req)
+	assert.Equal(t, "/api/users", req.URL.Path)
+	assert.Equal(t, http.StatusServiceUnavailable, rec.Code)
+}
diff --git a/middleware/proxy_test.go b/middleware/proxy_test.go
index 3a79b83..d003941 100644
--- a/middleware/proxy_test.go
+++ b/middleware/proxy_test.go
@@ -124,6 +124,7 @@ func TestProxy(t *testing.T) {
 	req.URL.Path = "/users/jack/orders/1"
 	e.ServeHTTP(rec, req)
 	assert.Equal(t, "/user/jack/order/1", req.URL.Path)
+	assert.Equal(t, http.StatusOK, rec.Code)
 
 	// ProxyTarget is set in context
 	contextObserver := func(next echo.HandlerFunc) echo.HandlerFunc {