aboutsummaryrefslogtreecommitdiffhomepage
path: root/http
diff options
context:
space:
mode:
authorGravatar Frédéric Guillot <fred@miniflux.net>2018-10-07 18:42:43 -0700
committerGravatar Frédéric Guillot <fred@miniflux.net>2018-10-08 15:31:58 -0700
commit1f58b37a5e86603b16e137031c36f37580e9d410 (patch)
tree337a7299e91fe7640b64489357dfe7c0f00e2313 /http
parentddfe969d6cbc8d23326cb9a3ca9a265d4e9d3e45 (diff)
Refactor HTTP response builder
Diffstat (limited to 'http')
-rw-r--r--http/response/builder.go134
-rw-r--r--http/response/builder_test.go351
-rw-r--r--http/response/doc.go10
-rw-r--r--http/response/html/doc.go10
-rw-r--r--http/response/html/html.go92
-rw-r--r--http/response/html/html_test.go212
-rw-r--r--http/response/json/doc.go10
-rw-r--r--http/response/json/json.go127
-rw-r--r--http/response/json/json_test.go313
-rw-r--r--http/response/response.go63
-rw-r--r--http/response/xml/doc.go10
-rw-r--r--http/response/xml/xml.go25
-rw-r--r--http/response/xml/xml_test.go83
13 files changed, 1268 insertions, 172 deletions
diff --git a/http/response/builder.go b/http/response/builder.go
new file mode 100644
index 0000000..f17c62e
--- /dev/null
+++ b/http/response/builder.go
@@ -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}
+}
diff --git a/http/response/builder_test.go b/http/response/builder_test.go
new file mode 100644
index 0000000..f4fef47
--- /dev/null
+++ b/http/response/builder_test.go
@@ -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)
+ }
+}
diff --git a/http/response/doc.go b/http/response/doc.go
new file mode 100644
index 0000000..007e0fa
--- /dev/null
+++ b/http/response/doc.go
@@ -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"
diff --git a/http/response/html/doc.go b/http/response/html/doc.go
new file mode 100644
index 0000000..91d3543
--- /dev/null
+++ b/http/response/html/doc.go
@@ -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"
diff --git a/http/response/html/html.go b/http/response/html/html.go
index 65a4649..f173fdb 100644
--- a/http/response/html/html.go
+++ b/http/response/html/html.go
@@ -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)
-
- 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"))
- }
+// 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", "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 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)
+
+ 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()
}
-// 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)
-
- if err != nil {
- logger.Error("[Bad Request] %v", err)
- w.Write([]byte("Bad Request: " + err.Error()))
- } else {
- w.Write([]byte("Bad Request"))
- }
+// 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()
}
-// 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"))
+// 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()
}
-// 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"))
+// Redirect redirects the user to another location.
+func Redirect(w http.ResponseWriter, r *http.Request, uri string) {
+ http.Redirect(w, r, uri, http.StatusFound)
}
diff --git a/http/response/html/html_test.go b/http/response/html/html_test.go
new file mode 100644
index 0000000..91c2b74
--- /dev/null
+++ b/http/response/html/html_test.go
@@ -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)
+ }
+}
diff --git a/http/response/json/doc.go b/http/response/json/doc.go
new file mode 100644
index 0000000..c2a74c1
--- /dev/null
+++ b/http/response/json/doc.go
@@ -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"
diff --git a/http/response/json/json.go b/http/response/json/json.go
index f19efb0..680a20d 100644
--- a/http/response/json/json.go
+++ b/http/response/json/json.go
@@ -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)
-// 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")))
+ 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()
}
-// 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)
+// Forbidden sends a forbidden error to the client.
+func Forbidden(w http.ResponseWriter, r *http.Request) {
+ logger.Error("[HTTP:Forbidden] %s", r.URL)
- if err != nil {
- w.Write(encodeError(err))
- }
+ 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()
}
-func commonHeaders(w http.ResponseWriter) {
- w.Header().Set("Accept", "application/json")
- w.Header().Set("Content-Type", "application/json; charset=utf-8")
+// 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", "application/json; charset=utf-8")
+ builder.WithBody(toJSONError(errors.New("Resource Not Found")))
+ builder.Write()
}
-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("")
}
diff --git a/http/response/json/json_test.go b/http/response/json/json_test.go
new file mode 100644
index 0000000..d22e468
--- /dev/null
+++ b/http/response/json/json_test.go
@@ -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)
+ }
+}
diff --git a/http/response/response.go b/http/response/response.go
deleted file mode 100644
index 4d73171..0000000
--- a/http/response/response.go
+++ /dev/null
@@ -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)
- }
-}
diff --git a/http/response/xml/doc.go b/http/response/xml/doc.go
new file mode 100644
index 0000000..908d2f9
--- /dev/null
+++ b/http/response/xml/doc.go
@@ -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"
diff --git a/http/response/xml/xml.go b/http/response/xml/xml.go
index bceb5f1..771a85e 100644
--- a/http/response/xml/xml.go
+++ b/http/response/xml/xml.go
@@ -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()
}
diff --git a/http/response/xml/xml_test.go b/http/response/xml/xml_test.go
new file mode 100644
index 0000000..ada6bd4
--- /dev/null
+++ b/http/response/xml/xml_test.go
@@ -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)
+ }
+ }
+}