Skip to content

Commit 9f8212f

Browse files
authored
feat: default prefix to /api if reverse proxy is used (#166)
Resolves #163.
1 parent bfd8199 commit 9f8212f

File tree

7 files changed

+389
-4
lines changed

7 files changed

+389
-4
lines changed

README.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ The recommended and only supported way for production deployments is to run the
1919

2020
:warning: If you do not configure a postgresql database, sqlite will automatically be used. Mount a persistent volume to the `/data` directory - this is where the sqlite database is stored. If you do not do this, you will lose all data every time the container is deleted.
2121

22+
If you serve the backend with a reverse proxy under a different path than `/`, note the following:
23+
24+
- Your reverse proxy must set the `x-forwarded-host` header so that correct URIs are generated
25+
- If you do not serve the backend at the prefix `/api` (required by the [frontend](https://github.com/envelope-zero/frontend)), your reverse proxy needs to set the `x-forwarded-prefix` header
26+
2227
The backend can be configured with the following environment variables. None are required.
2328

2429
| Name | Type | Default | Description |
@@ -67,8 +72,6 @@ ingress:
6772
- envelope-zero.example.com
6873
```
6974
70-
If you do not use the root path `/`, but a prefix, make sure your reverse proxy writes the used prefix in the `x-forwarded-prefix` header. That header is used by the backend to generate the correct URLs for resources.
71-
7275
## Supported Versions
7376
7477
This project is under heavy development. Therefore, only the latest release is supported.

internal/httputil/error_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package httputil_test
2+
3+
import (
4+
"errors"
5+
"net/http"
6+
"net/http/httptest"
7+
"strconv"
8+
"testing"
9+
"time"
10+
11+
"github.com/envelope-zero/backend/internal/httputil"
12+
"github.com/envelope-zero/backend/internal/test"
13+
"github.com/gin-gonic/gin"
14+
"github.com/stretchr/testify/assert"
15+
"gorm.io/gorm"
16+
)
17+
18+
func TestFetchErrorHandlerErrRecordNotFound(t *testing.T) {
19+
w := httptest.NewRecorder()
20+
c, r := gin.CreateTestContext(w)
21+
22+
r.GET("/", func(ctx *gin.Context) {
23+
httputil.FetchErrorHandler(c, gorm.ErrRecordNotFound)
24+
})
25+
26+
// Check without reverse proxy headers
27+
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
28+
r.ServeHTTP(w, c.Request)
29+
assert.Equal(t, http.StatusNotFound, w.Code)
30+
}
31+
32+
func TestFetchErrorHandlerStrconvNumError(t *testing.T) {
33+
w := httptest.NewRecorder()
34+
c, r := gin.CreateTestContext(w)
35+
36+
r.GET("/", func(ctx *gin.Context) {
37+
httputil.FetchErrorHandler(c, &strconv.NumError{})
38+
})
39+
40+
// Check without reverse proxy headers
41+
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
42+
r.ServeHTTP(w, c.Request)
43+
assert.Equal(t, http.StatusBadRequest, w.Code)
44+
assert.Equal(t, "An ID specified in the query string was not a valid uint64", test.DecodeError(t, w.Body.Bytes()))
45+
}
46+
47+
func TestFetchErrorHandlerTimeParseError(t *testing.T) {
48+
w := httptest.NewRecorder()
49+
c, r := gin.CreateTestContext(w)
50+
51+
r.GET("/", func(ctx *gin.Context) {
52+
httputil.FetchErrorHandler(c, &time.ParseError{})
53+
})
54+
55+
// Check without reverse proxy headers
56+
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
57+
r.ServeHTTP(w, c.Request)
58+
assert.Equal(t, http.StatusBadRequest, w.Code)
59+
assert.Contains(t, test.DecodeError(t, w.Body.Bytes()), "parsing time")
60+
}
61+
62+
func TestFetchErrorHandlerInternalServerError(t *testing.T) {
63+
w := httptest.NewRecorder()
64+
c, r := gin.CreateTestContext(w)
65+
66+
r.GET("/", func(ctx *gin.Context) {
67+
httputil.FetchErrorHandler(c, errors.New("Some random error"))
68+
})
69+
70+
// Check without reverse proxy headers
71+
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
72+
r.ServeHTTP(w, c.Request)
73+
assert.Equal(t, http.StatusInternalServerError, w.Code)
74+
assert.Contains(t, test.DecodeError(t, w.Body.Bytes()), "an error occured on the server during your request")
75+
}

internal/httputil/options_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package httputil_test
2+
3+
import (
4+
"net/http"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/envelope-zero/backend/internal/httputil"
9+
"github.com/gin-gonic/gin"
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestOptionsGet(t *testing.T) {
14+
w := httptest.NewRecorder()
15+
c, r := gin.CreateTestContext(w)
16+
17+
r.GET("/", httputil.OptionsGet)
18+
19+
// Check without reverse proxy headers
20+
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
21+
c.Request.Host = "example.com"
22+
r.ServeHTTP(w, c.Request)
23+
24+
assert.Equal(t, "GET", w.Header().Get("allow"))
25+
assert.Equal(t, http.StatusNoContent, w.Code)
26+
}
27+
28+
func TestOptionsPost(t *testing.T) {
29+
w := httptest.NewRecorder()
30+
c, r := gin.CreateTestContext(w)
31+
32+
r.GET("/", httputil.OptionsGetPost)
33+
34+
// Check without reverse proxy headers
35+
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
36+
c.Request.Host = "example.com"
37+
r.ServeHTTP(w, c.Request)
38+
39+
assert.Equal(t, "GET, POST", w.Header().Get("allow"))
40+
assert.Equal(t, http.StatusNoContent, w.Code)
41+
}
42+
43+
func TestOptionsGetPatchDelete(t *testing.T) {
44+
w := httptest.NewRecorder()
45+
c, r := gin.CreateTestContext(w)
46+
47+
r.GET("/", httputil.OptionsGetPatchDelete)
48+
49+
// Check without reverse proxy headers
50+
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
51+
c.Request.Host = "example.com"
52+
r.ServeHTTP(w, c.Request)
53+
54+
assert.Equal(t, "GET, PATCH, DELETE", w.Header().Get("allow"))
55+
assert.Equal(t, http.StatusNoContent, w.Code)
56+
}

internal/httputil/request.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package httputil
22

33
import (
44
"errors"
5+
"fmt"
56
"io"
67
"net/http"
78
"strconv"
@@ -19,8 +20,31 @@ func RequestHost(c *gin.Context) string {
1920
scheme = "https"
2021
}
2122

22-
forwardedPrefix := c.Request.Header.Get("x-forwarded-prefix")
23-
return scheme + "://" + c.Request.Host + forwardedPrefix
23+
// We can reasonably expect a reverse proxy to set x-forwarded-host
24+
// as it is a de-facto standard.
25+
//
26+
// If it is set, we use it to construct the links and use the
27+
// x-forwarded-prefix header as prefix. If that is unset,
28+
// fall back to "/api"
29+
//
30+
// If no proxy is detected, don’t do anything.
31+
host := c.Request.Host
32+
var forwardedPrefix string
33+
34+
fmt.Println(c.Request.Header)
35+
36+
xForwardedHost := c.Request.Header.Get("x-forwarded-host")
37+
if xForwardedHost != "" {
38+
host = xForwardedHost
39+
40+
forwardedPrefix = c.Request.Header.Get("x-forwarded-prefix")
41+
42+
if forwardedPrefix == "" {
43+
forwardedPrefix = "/api"
44+
}
45+
}
46+
47+
return scheme + "://" + host + forwardedPrefix
2448
}
2549

2650
// RequestPathV1 returns the URL with the prefix for API v1.

0 commit comments

Comments
 (0)