1
0
Fork 0
mirror of https://github.com/miniflux/v2.git synced 2025-08-21 18:11:09 +00:00

Refactor HTTP response builder

This commit is contained in:
Frédéric Guillot 2018-10-07 18:42:43 -07:00
parent ddfe969d6c
commit 1f58b37a5e
94 changed files with 1701 additions and 644 deletions

134
http/response/builder.go Normal file
View file

@ -0,0 +1,134 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package response // import "miniflux.app/http/response"
import (
"compress/flate"
"compress/gzip"
"fmt"
"net/http"
"strings"
"time"
)
const compressionThreshold = 1024
// Builder generates HTTP responses.
type Builder struct {
w http.ResponseWriter
r *http.Request
statusCode int
headers map[string]string
enableCompression bool
body interface{}
}
// WithStatus uses the given status code to build the response.
func (b *Builder) WithStatus(statusCode int) *Builder {
b.statusCode = statusCode
return b
}
// WithHeader adds the given HTTP header to the response.
func (b *Builder) WithHeader(key, value string) *Builder {
b.headers[key] = value
return b
}
// WithBody uses the given body to build the response.
func (b *Builder) WithBody(body interface{}) *Builder {
b.body = body
return b
}
// WithAttachment forces the document to be downloaded by the web browser.
func (b *Builder) WithAttachment(filename string) *Builder {
b.headers["Content-Disposition"] = fmt.Sprintf("attachment; filename=%s", filename)
return b
}
// WithoutCompression disables HTTP compression.
func (b *Builder) WithoutCompression() *Builder {
b.enableCompression = false
return b
}
// WithCaching adds caching headers to the response.
func (b *Builder) WithCaching(etag string, duration time.Duration, callback func(*Builder)) {
b.headers["ETag"] = etag
b.headers["Cache-Control"] = "public"
b.headers["Expires"] = time.Now().Add(duration).Format(time.RFC1123)
if etag == b.r.Header.Get("If-None-Match") {
b.statusCode = http.StatusNotModified
b.body = nil
b.Write()
} else {
callback(b)
}
}
// Write generates the HTTP response.
func (b *Builder) Write() {
if b.body == nil {
b.writeHeaders()
return
}
switch v := b.body.(type) {
case []byte:
b.compress(v)
case string:
b.compress([]byte(v))
case error:
b.compress([]byte(v.Error()))
}
}
func (b *Builder) writeHeaders() {
b.headers["X-XSS-Protection"] = "1; mode=block"
b.headers["X-Content-Type-Options"] = "nosniff"
b.headers["X-Frame-Options"] = "DENY"
b.headers["Content-Security-Policy"] = "default-src 'self'; img-src *; media-src *; frame-src *; child-src *"
for key, value := range b.headers {
b.w.Header().Set(key, value)
}
b.w.WriteHeader(b.statusCode)
}
func (b *Builder) compress(data []byte) {
if b.enableCompression && len(data) > compressionThreshold {
acceptEncoding := b.r.Header.Get("Accept-Encoding")
switch {
case strings.Contains(acceptEncoding, "gzip"):
b.headers["Content-Encoding"] = "gzip"
b.writeHeaders()
gzipWriter := gzip.NewWriter(b.w)
defer gzipWriter.Close()
gzipWriter.Write(data)
return
case strings.Contains(acceptEncoding, "deflate"):
b.headers["Content-Encoding"] = "deflate"
b.writeHeaders()
flateWriter, _ := flate.NewWriter(b.w, -1)
defer flateWriter.Close()
flateWriter.Write(data)
return
}
}
b.writeHeaders()
b.w.Write(data)
}
// New creates a new response builder.
func New(w http.ResponseWriter, r *http.Request) *Builder {
return &Builder{w: w, r: r, statusCode: http.StatusOK, headers: make(map[string]string), enableCompression: true}
}

View file

@ -0,0 +1,351 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package response // import "miniflux.app/http/response"
import (
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestResponseHasCommonHeaders(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).Write()
})
handler.ServeHTTP(w, r)
resp := w.Result()
headers := map[string]string{
"X-XSS-Protection": "1; mode=block",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Content-Security-Policy": "default-src 'self'; img-src *; media-src *; frame-src *; child-src *",
}
for header, expected := range headers {
actual := resp.Header.Get(header)
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}
}
func TestBuildResponseWithCustomStatusCode(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithStatus(http.StatusNotAcceptable).Write()
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusNotAcceptable
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
}
func TestBuildResponseWithCustomHeader(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithHeader("X-My-Header", "Value").Write()
})
handler.ServeHTTP(w, r)
resp := w.Result()
expected := "Value"
actual := resp.Header.Get("X-My-Header")
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}
func TestBuildResponseWithAttachment(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithAttachment("my_file.pdf").Write()
})
handler.ServeHTTP(w, r)
resp := w.Result()
expected := "attachment; filename=my_file.pdf"
actual := resp.Header.Get("Content-Disposition")
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}
func TestBuildResponseWithError(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithBody(errors.New("Some error")).Write()
})
handler.ServeHTTP(w, r)
expectedBody := `Some error`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
}
func TestBuildResponseWithByteBody(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithBody([]byte("body")).Write()
})
handler.ServeHTTP(w, r)
expectedBody := `body`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
}
func TestBuildResponseWithCachingEnabled(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithCaching("etag", 1*time.Minute, func(b *Builder) {
b.WithBody("cached body")
b.Write()
})
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusOK
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `cached body`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedHeader := "public"
actualHeader := resp.Header.Get("Cache-Control")
if actualHeader != expectedHeader {
t.Fatalf(`Unexpected cache control header, got %q instead of %q`, actualHeader, expectedHeader)
}
if resp.Header.Get("Expires") == "" {
t.Fatalf(`Expires header should not be empty`)
}
}
func TestBuildResponseWithCachingAndEtag(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
r.Header.Set("If-None-Match", "etag")
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithCaching("etag", 1*time.Minute, func(b *Builder) {
b.WithBody("cached body")
b.Write()
})
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusNotModified
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := ``
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedHeader := "public"
actualHeader := resp.Header.Get("Cache-Control")
if actualHeader != expectedHeader {
t.Fatalf(`Unexpected cache control header, got %q instead of %q`, actualHeader, expectedHeader)
}
if resp.Header.Get("Expires") == "" {
t.Fatalf(`Expires header should not be empty`)
}
}
func TestBuildResponseWithGzipCompression(t *testing.T) {
body := strings.Repeat("a", compressionThreshold+1)
r, err := http.NewRequest("GET", "/", nil)
r.Header.Set("Accept-Encoding", "gzip, deflate, br")
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithBody(body).Write()
})
handler.ServeHTTP(w, r)
resp := w.Result()
expected := "gzip"
actual := resp.Header.Get("Content-Encoding")
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}
func TestBuildResponseWithDeflateCompression(t *testing.T) {
body := strings.Repeat("a", compressionThreshold+1)
r, err := http.NewRequest("GET", "/", nil)
r.Header.Set("Accept-Encoding", "deflate")
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithBody(body).Write()
})
handler.ServeHTTP(w, r)
resp := w.Result()
expected := "deflate"
actual := resp.Header.Get("Content-Encoding")
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}
func TestBuildResponseWithCompressionDisabled(t *testing.T) {
body := strings.Repeat("a", compressionThreshold+1)
r, err := http.NewRequest("GET", "/", nil)
r.Header.Set("Accept-Encoding", "deflate")
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithBody(body).WithoutCompression().Write()
})
handler.ServeHTTP(w, r)
resp := w.Result()
expected := ""
actual := resp.Header.Get("Content-Encoding")
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}
func TestBuildResponseWithDeflateCompressionAndSmallPayload(t *testing.T) {
body := strings.Repeat("a", compressionThreshold)
r, err := http.NewRequest("GET", "/", nil)
r.Header.Set("Accept-Encoding", "deflate")
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithBody(body).Write()
})
handler.ServeHTTP(w, r)
resp := w.Result()
expected := ""
actual := resp.Header.Get("Content-Encoding")
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}
func TestBuildResponseWithoutCompressionHeader(t *testing.T) {
body := strings.Repeat("a", compressionThreshold+1)
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
New(w, r).WithBody(body).Write()
})
handler.ServeHTTP(w, r)
resp := w.Result()
expected := ""
actual := resp.Header.Get("Content-Encoding")
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}

10
http/response/doc.go Normal file
View file

@ -0,0 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/*
Package response contains everything related to HTTP responses.
*/
package response // import "miniflux.app/http/response"

10
http/response/html/doc.go Normal file
View file

@ -0,0 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/*
Package html contains HTML response functions.
*/
package html // import "miniflux.app/http/response/html"

View file

@ -1,6 +1,6 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package html // import "miniflux.app/http/response/html"
@ -11,48 +11,64 @@ import (
"miniflux.app/logger"
)
// OK writes a standard HTML response.
func OK(w http.ResponseWriter, r *http.Request, b []byte) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
response.Compress(w, r, b)
// OK creates a new HTML response with a 200 status code.
func OK(w http.ResponseWriter, r *http.Request, body interface{}) {
builder := response.New(w, r)
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody(body)
builder.Write()
}
// ServerError sends a 500 error to the browser.
func ServerError(w http.ResponseWriter, err error) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusInternalServerError)
// ServerError sends an internal error to the client.
func ServerError(w http.ResponseWriter, r *http.Request, err error) {
logger.Error("[HTTP:Internal Server Error] %s => %v", r.URL, err)
if err != nil {
logger.Error("[Internal Server Error] %v", err)
w.Write([]byte("Internal Server Error: " + err.Error()))
} else {
w.Write([]byte("Internal Server Error"))
}
builder := response.New(w, r)
builder.WithStatus(http.StatusInternalServerError)
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody(err)
builder.Write()
}
// BadRequest sends a 400 error to the browser.
func BadRequest(w http.ResponseWriter, err error) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusBadRequest)
// BadRequest sends a bad request error to the client.
func BadRequest(w http.ResponseWriter, r *http.Request, err error) {
logger.Error("[HTTP:Bad Request] %s => %v", r.URL, err)
if err != nil {
logger.Error("[Bad Request] %v", err)
w.Write([]byte("Bad Request: " + err.Error()))
} else {
w.Write([]byte("Bad Request"))
}
builder := response.New(w, r)
builder.WithStatus(http.StatusBadRequest)
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody(err)
builder.Write()
}
// NotFound sends a 404 error to the browser.
func NotFound(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("Page Not Found"))
// Forbidden sends a forbidden error to the client.
func Forbidden(w http.ResponseWriter, r *http.Request) {
logger.Error("[HTTP:Forbidden] %s", r.URL)
builder := response.New(w, r)
builder.WithStatus(http.StatusForbidden)
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody("Access Forbidden")
builder.Write()
}
// Forbidden sends a 403 error to the browser.
func Forbidden(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("Access Forbidden"))
// NotFound sends a page not found error to the client.
func NotFound(w http.ResponseWriter, r *http.Request) {
logger.Error("[HTTP:Not Found] %s", r.URL)
builder := response.New(w, r)
builder.WithStatus(http.StatusNotFound)
builder.WithHeader("Content-Type", "text/html; charset=utf-8")
builder.WithHeader("Cache-Control", "no-cache, max-age=0, must-revalidate, no-store")
builder.WithBody("Page Not Found")
builder.Write()
}
// Redirect redirects the user to another location.
func Redirect(w http.ResponseWriter, r *http.Request, uri string) {
http.Redirect(w, r, uri, http.StatusFound)
}

View file

@ -0,0 +1,212 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package html // import "miniflux.app/http/response/html"
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
)
func TestOKResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
OK(w, r, "Some HTML")
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusOK
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `Some HTML`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
headers := map[string]string{
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-cache, max-age=0, must-revalidate, no-store",
}
for header, expected := range headers {
actual := resp.Header.Get(header)
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}
}
func TestServerErrorResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ServerError(w, r, errors.New("Some error"))
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusInternalServerError
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `Some error`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "text/html; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestBadRequestResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
BadRequest(w, r, errors.New("Some error"))
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusBadRequest
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `Some error`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "text/html; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestForbiddenResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Forbidden(w, r)
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusForbidden
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `Access Forbidden`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "text/html; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestNotFoundResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
NotFound(w, r)
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusNotFound
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `Page Not Found`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "text/html; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestRedirectResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Redirect(w, r, "/path")
})
handler.ServeHTTP(w, r)
resp := w.Result()
defer resp.Body.Close()
expectedStatusCode := http.StatusFound
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedResult := "/path"
actualResult := resp.Header.Get("Location")
if actualResult != expectedResult {
t.Fatalf(`Unexpected redirect location, got %q instead of %q`, actualResult, expectedResult)
}
}

10
http/response/json/doc.go Normal file
View file

@ -0,0 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/*
Package json contains JSON response functions.
*/
package json // import "miniflux.app/http/response/json"

View file

@ -1,6 +1,6 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package json // import "miniflux.app/http/response/json"
@ -13,93 +13,98 @@ import (
"miniflux.app/logger"
)
// OK sends a JSON response with the status code 200.
func OK(w http.ResponseWriter, r *http.Request, v interface{}) {
commonHeaders(w)
response.Compress(w, r, toJSON(v))
// OK creates a new JSON response with a 200 status code.
func OK(w http.ResponseWriter, r *http.Request, body interface{}) {
builder := response.New(w, r)
builder.WithHeader("Content-Type", "application/json; charset=utf-8")
builder.WithBody(toJSON(body))
builder.Write()
}
// Created sends a JSON response with the status code 201.
func Created(w http.ResponseWriter, v interface{}) {
commonHeaders(w)
w.WriteHeader(http.StatusCreated)
w.Write(toJSON(v))
// Created sends a created response to the client.
func Created(w http.ResponseWriter, r *http.Request, body interface{}) {
builder := response.New(w, r)
builder.WithStatus(http.StatusCreated)
builder.WithHeader("Content-Type", "application/json; charset=utf-8")
builder.WithBody(toJSON(body))
builder.Write()
}
// NoContent sends a JSON response with the status code 204.
func NoContent(w http.ResponseWriter) {
commonHeaders(w)
w.WriteHeader(http.StatusNoContent)
// NoContent sends a no content response to the client.
func NoContent(w http.ResponseWriter, r *http.Request) {
builder := response.New(w, r)
builder.WithStatus(http.StatusNoContent)
builder.WithHeader("Content-Type", "application/json; charset=utf-8")
builder.Write()
}
// NotFound sends a JSON response with the status code 404.
func NotFound(w http.ResponseWriter, err error) {
logger.Error("[Not Found] %v", err)
commonHeaders(w)
w.WriteHeader(http.StatusNotFound)
w.Write(encodeError(err))
// ServerError sends an internal error to the client.
func ServerError(w http.ResponseWriter, r *http.Request, err error) {
logger.Error("[HTTP:Internal Server Error] %s => %v", r.URL, err)
builder := response.New(w, r)
builder.WithStatus(http.StatusInternalServerError)
builder.WithHeader("Content-Type", "application/json; charset=utf-8")
builder.WithBody(toJSONError(err))
builder.Write()
}
// ServerError sends a JSON response with the status code 500.
func ServerError(w http.ResponseWriter, err error) {
logger.Error("[Internal Server Error] %v", err)
commonHeaders(w)
w.WriteHeader(http.StatusInternalServerError)
// BadRequest sends a bad request error to the client.
func BadRequest(w http.ResponseWriter, r *http.Request, err error) {
logger.Error("[HTTP:Bad Request] %s => %v", r.URL, err)
if err != nil {
w.Write(encodeError(err))
}
builder := response.New(w, r)
builder.WithStatus(http.StatusBadRequest)
builder.WithHeader("Content-Type", "application/json; charset=utf-8")
builder.WithBody(toJSONError(err))
builder.Write()
}
// Forbidden sends a JSON response with the status code 403.
func Forbidden(w http.ResponseWriter) {
logger.Info("[Forbidden]")
commonHeaders(w)
w.WriteHeader(http.StatusForbidden)
w.Write(encodeError(errors.New("Access Forbidden")))
// Unauthorized sends a not authorized error to the client.
func Unauthorized(w http.ResponseWriter, r *http.Request) {
logger.Error("[HTTP:Unauthorized] %s", r.URL)
builder := response.New(w, r)
builder.WithStatus(http.StatusUnauthorized)
builder.WithHeader("Content-Type", "application/json; charset=utf-8")
builder.WithBody(toJSONError(errors.New("Access Unauthorized")))
builder.Write()
}
// Unauthorized sends a JSON response with the status code 401.
func Unauthorized(w http.ResponseWriter) {
commonHeaders(w)
w.WriteHeader(http.StatusUnauthorized)
w.Write(encodeError(errors.New("Access Unauthorized")))
// Forbidden sends a forbidden error to the client.
func Forbidden(w http.ResponseWriter, r *http.Request) {
logger.Error("[HTTP:Forbidden] %s", r.URL)
builder := response.New(w, r)
builder.WithStatus(http.StatusForbidden)
builder.WithHeader("Content-Type", "application/json; charset=utf-8")
builder.WithBody(toJSONError(errors.New("Access Forbidden")))
builder.Write()
}
// BadRequest sends a JSON response with the status code 400.
func BadRequest(w http.ResponseWriter, err error) {
logger.Error("[Bad Request] %v", err)
commonHeaders(w)
w.WriteHeader(http.StatusBadRequest)
// NotFound sends a page not found error to the client.
func NotFound(w http.ResponseWriter, r *http.Request) {
logger.Error("[HTTP:Not Found] %s", r.URL)
if err != nil {
w.Write(encodeError(err))
}
builder := response.New(w, r)
builder.WithStatus(http.StatusNotFound)
builder.WithHeader("Content-Type", "application/json; charset=utf-8")
builder.WithBody(toJSONError(errors.New("Resource Not Found")))
builder.Write()
}
func commonHeaders(w http.ResponseWriter) {
w.Header().Set("Accept", "application/json")
w.Header().Set("Content-Type", "application/json; charset=utf-8")
}
func encodeError(err error) []byte {
func toJSONError(err error) []byte {
type errorMsg struct {
ErrorMessage string `json:"error_message"`
}
tmp := errorMsg{ErrorMessage: err.Error()}
data, err := json.Marshal(tmp)
if err != nil {
logger.Error("json encoding error: %v", err)
}
return data
return toJSON(errorMsg{ErrorMessage: err.Error()})
}
func toJSON(v interface{}) []byte {
b, err := json.Marshal(v)
if err != nil {
logger.Error("json encoding error: %v", err)
logger.Error("[HTTP:JSON] %v", err)
return []byte("")
}

View file

@ -0,0 +1,313 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package json // import "miniflux.app/http/response/json"
import (
"errors"
"net/http"
"net/http/httptest"
"testing"
)
func TestOKResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
OK(w, r, map[string]string{"key": "value"})
})
handler.ServeHTTP(w, r)
resp := w.Result()
defer resp.Body.Close()
expectedStatusCode := http.StatusOK
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `{"key":"value"}`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %q instead of %q`, actualBody, expectedBody)
}
expectedContentType := "application/json; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestCreatedResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Created(w, r, map[string]string{"key": "value"})
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusCreated
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `{"key":"value"}`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "application/json; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestNoContentResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
NoContent(w, r)
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusNoContent
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := ``
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "application/json; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestServerErrorResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ServerError(w, r, errors.New("some error"))
})
handler.ServeHTTP(w, r)
resp := w.Result()
defer resp.Body.Close()
expectedStatusCode := http.StatusInternalServerError
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `{"error_message":"some error"}`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %q instead of %q`, actualBody, expectedBody)
}
expectedContentType := "application/json; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestBadRequestResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
BadRequest(w, r, errors.New("Some Error"))
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusBadRequest
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `{"error_message":"Some Error"}`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "application/json; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestUnauthorizedResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Unauthorized(w, r)
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusUnauthorized
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `{"error_message":"Access Unauthorized"}`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "application/json; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestForbiddenResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Forbidden(w, r)
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusForbidden
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `{"error_message":"Access Forbidden"}`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "application/json; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestNotFoundResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
NotFound(w, r)
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusNotFound
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `{"error_message":"Resource Not Found"}`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "application/json; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestBuildInvalidJSONResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
OK(w, r, make(chan int))
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusOK
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := ``
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "application/json; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}

View file

@ -1,63 +0,0 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package response // import "miniflux.app/http/response"
import (
"compress/flate"
"compress/gzip"
"net/http"
"strings"
"time"
)
// Redirect redirects the user to another location.
func Redirect(w http.ResponseWriter, r *http.Request, path string) {
http.Redirect(w, r, path, http.StatusFound)
}
// NotModified sends a response with a 304 status code.
func NotModified(w http.ResponseWriter) {
w.WriteHeader(http.StatusNotModified)
}
// Cache returns a response with caching headers.
func Cache(w http.ResponseWriter, r *http.Request, mimeType, etag string, data []byte, duration time.Duration) {
w.Header().Set("Content-Type", mimeType)
w.Header().Set("ETag", etag)
w.Header().Set("Cache-Control", "public")
w.Header().Set("Expires", time.Now().Add(duration).Format(time.RFC1123))
if etag == r.Header.Get("If-None-Match") {
w.WriteHeader(http.StatusNotModified)
return
}
switch mimeType {
case "text/javascript; charset=utf-8", "text/css; charset=utf-8":
Compress(w, r, data)
default:
w.Write(data)
}
}
// Compress the response sent to the browser.
func Compress(w http.ResponseWriter, r *http.Request, data []byte) {
acceptEncoding := r.Header.Get("Accept-Encoding")
switch {
case strings.Contains(acceptEncoding, "gzip"):
w.Header().Set("Content-Encoding", "gzip")
gzipWriter := gzip.NewWriter(w)
defer gzipWriter.Close()
gzipWriter.Write(data)
case strings.Contains(acceptEncoding, "deflate"):
w.Header().Set("Content-Encoding", "deflate")
flateWriter, _ := flate.NewWriter(w, -1)
defer flateWriter.Close()
flateWriter.Write(data)
default:
w.Write(data)
}
}

10
http/response/xml/doc.go Normal file
View file

@ -0,0 +1,10 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
/*
Package xml contains XML response functions.
*/
package xml // import "miniflux.app/http/response/xml"

View file

@ -5,19 +5,24 @@
package xml // import "miniflux.app/http/response/xml"
import (
"fmt"
"net/http"
"miniflux.app/http/response"
)
// OK sends a XML document.
func OK(w http.ResponseWriter, data string) {
w.Header().Set("Content-Type", "text/xml")
w.Write([]byte(data))
// OK writes a standard XML response with a status 200 OK.
func OK(w http.ResponseWriter, r *http.Request, body interface{}) {
builder := response.New(w, r)
builder.WithHeader("Content-Type", "text/xml; charset=utf-8")
builder.WithBody(body)
builder.Write()
}
// Attachment forces the download of a XML document.
func Attachment(w http.ResponseWriter, filename, data string) {
w.Header().Set("Content-Type", "text/xml")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
w.Write([]byte(data))
// Attachment forces the XML document to be downloaded by the web browser.
func Attachment(w http.ResponseWriter, r *http.Request, filename string, body interface{}) {
builder := response.New(w, r)
builder.WithHeader("Content-Type", "text/xml; charset=utf-8")
builder.WithAttachment(filename)
builder.WithBody(body)
builder.Write()
}

View file

@ -0,0 +1,83 @@
// Copyright 2018 Frédéric Guillot. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package xml // import "miniflux.app/http/response/xml"
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestOKResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
OK(w, r, "Some XML")
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusOK
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `Some XML`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
expectedContentType := "text/xml; charset=utf-8"
actualContentType := resp.Header.Get("Content-Type")
if actualContentType != expectedContentType {
t.Fatalf(`Unexpected content type, got %q instead of %q`, actualContentType, expectedContentType)
}
}
func TestAttachmentResponse(t *testing.T) {
r, err := http.NewRequest("GET", "/", nil)
if err != nil {
t.Fatal(err)
}
w := httptest.NewRecorder()
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Attachment(w, r, "file.xml", "Some XML")
})
handler.ServeHTTP(w, r)
resp := w.Result()
expectedStatusCode := http.StatusOK
if resp.StatusCode != expectedStatusCode {
t.Fatalf(`Unexpected status code, got %d instead of %d`, resp.StatusCode, expectedStatusCode)
}
expectedBody := `Some XML`
actualBody := w.Body.String()
if actualBody != expectedBody {
t.Fatalf(`Unexpected body, got %s instead of %s`, actualBody, expectedBody)
}
headers := map[string]string{
"Content-Type": "text/xml; charset=utf-8",
"Content-Disposition": "attachment; filename=file.xml",
}
for header, expected := range headers {
actual := resp.Header.Get(header)
if actual != expected {
t.Fatalf(`Unexpected header value, got %q instead of %q`, actual, expected)
}
}
}