Skip to content
This repository has been archived by the owner on Oct 8, 2020. It is now read-only.

Commit

Permalink
Merge pull request #28 from Financial-Times/bugfix/propagate-client-e…
Browse files Browse the repository at this point in the history
…rrors

Propagate client errors
  • Loading branch information
Keith Hatton authored Mar 16, 2018
2 parents 1153a75 + d485864 commit 38ded06
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 36 deletions.
78 changes: 63 additions & 15 deletions app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ import (
"github.com/stretchr/testify/assert"
)

const (
sourceAppName = "Native Content Service"
transformAppName = "Native Content Transformer Service"
previewableUuid = "d7db73ec-cf53-11e5-92a1-c5e23ef99c77"
unpreviewableUuid = "b82cc800-87c1-4983-83d2-0bd677f49b2b"
)

var contentPreviewService *httptest.Server
var methodeApiMock *httptest.Server
var methodeArticleTransformerMock *httptest.Server

const sourceAppName = "Native Content Service"
const transformAppName = "Native Content Transformer Service"
var methodeApiAuth string

func startMethodeApiMock(status string) {
r := mux.NewRouter()
Expand All @@ -49,18 +54,26 @@ func methodeApiHandlerMock(w http.ResponseWriter, r *http.Request) {
w.Header().Set(tid.TransactionIDHeader, "tid_w58gqvazux")
}

if r.Header.Get("Authorization") == "Basic default" && uuid == "d7db73ec-cf53-11e5-92a1-c5e23ef99c77" {
file, err := os.Open("test-resources/methode-api-output.json")
if err != nil {
return
if r.Header.Get("Authorization") == "Basic " + methodeApiAuth {
switch uuid {
case previewableUuid:
file, err := os.Open("test-resources/methode-api-output.json")
if err != nil {
return
}
defer file.Close()
io.Copy(w, file)

case unpreviewableUuid:
w.WriteHeader(http.StatusUnprocessableEntity)
json.NewEncoder(w).Encode("{\"message\":\"422 Unprocessable entity\"}")
default:
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode("{\"message\":\"404 Not Found - null\"}")
}
defer file.Close()
io.Copy(w, file)
} else {
w.WriteHeader(http.StatusNotFound)
json.NewEncoder(w).Encode("{\"message\":\"404 Not Found - null\"}")
w.WriteHeader(http.StatusUnauthorized)
}

}

func unhappyHandler(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -103,7 +116,7 @@ func methodeArticleTransformerHandlerMock(w http.ResponseWriter, r *http.Request
w.Header().Set(tid.TransactionIDHeader, "tid_w58gqvazux")
}

if shortF.Uuid == "d7db73ec-cf53-11e5-92a1-c5e23ef99c77" {
if shortF.Uuid == previewableUuid {
file, err := os.Open("test-resources/methode-article-transformer-output.json")
if err != nil {
return
Expand Down Expand Up @@ -134,6 +147,7 @@ func stopServices() {
}

func startContentPreviewService() {
methodeApiAuth = "default"
methodeApiUrl := methodeApiMock.URL + "/eom-file/"
nativeContentAppHealthUri := methodeApiMock.URL + "/build-info"
methodArticleTransformerUrl := methodeArticleTransformerMock.URL + "/map"
Expand All @@ -144,7 +158,7 @@ func startContentPreviewService() {
appName: "Content Preview",
appPort: "8084",
sourceAppName: sourceAppName,
sourceAppAuth: "default",
sourceAppAuth: methodeApiAuth,
sourceAppUri: methodeApiUrl,
sourceAppHealthUri: nativeContentAppHealthUri,
sourceAppPanicGuide: "panic guide",
Expand All @@ -171,7 +185,7 @@ func TestShouldReturn200AndTransformerOutput(t *testing.T) {
startMethodeArticleTransformerMock("happy")
startContentPreviewService()
defer stopServices()
resp, err := http.Get(contentPreviewService.URL + "/content-preview/d7db73ec-cf53-11e5-92a1-c5e23ef99c77")
resp, err := http.Get(contentPreviewService.URL + "/content-preview/" + previewableUuid)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -212,6 +226,40 @@ func TestShouldReturn404(t *testing.T) {

}

func TestShouldReturn422(t *testing.T) {
startMethodeApiMock("happy")
startMethodeArticleTransformerMock("happy")
startContentPreviewService()
defer stopServices()

resp, err := http.Get(contentPreviewService.URL + "/content-preview/" + unpreviewableUuid)
if err != nil {
panic(err)
}
defer resp.Body.Close()

assert.Equal(t, http.StatusUnprocessableEntity, resp.StatusCode, "Response status")

contentPreviewService.Close()
}

func TestInvalidAuth(t *testing.T) {
startMethodeApiMock("happy")
startMethodeArticleTransformerMock("happy")
startContentPreviewService()
defer stopServices()

methodeApiAuth = "frodo"

resp, err := http.Get(contentPreviewService.URL + "/content-preview/" + previewableUuid)
if err != nil {
panic(err)
}
defer resp.Body.Close()

assert.Equal(t, 5, resp.StatusCode / 100, "Response status should be 5xx")
}

func TestShouldReturn503whenMethodeApiIsNotHappy(t *testing.T) {
startMethodeApiMock("unhappy")
startMethodeArticleTransformerMock("happy")
Expand Down
80 changes: 59 additions & 21 deletions handlers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package main

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -25,22 +27,24 @@ func (h ContentHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := tid.TransactionAwareContext(context.Background(), r.Header.Get(tid.TransactionIDHeader))
ctx = context.WithValue(ctx, uuidKey, uuid)
w.Header().Set("Content-Type", "application/json; charset=utf-8")
success, nativeContentSourceAppResponse := h.getNativeContent(ctx, w)

if !success {
nativeContentSourceAppResponse, err := h.getNativeContent(ctx, w)
if err != nil {
return
}
success, transformAppResponse := h.getTransformedContent(ctx, *nativeContentSourceAppResponse, w)
if !success {
nativeContentSourceAppResponse.Body.Close()
defer nativeContentSourceAppResponse.Body.Close()

transformAppResponse, err := h.getTransformedContent(ctx, *nativeContentSourceAppResponse, w)
if err != nil {
return
}
defer transformAppResponse.Body.Close()

io.Copy(w, transformAppResponse.Body)
transformAppResponse.Body.Close()
h.metrics.recordResponseEvent()
}

func (h ContentHandler) getNativeContent(ctx context.Context, w http.ResponseWriter) (ok bool, resp *http.Response) {
func (h ContentHandler) getNativeContent(ctx context.Context, w http.ResponseWriter) (*http.Response, error) {
uuid := ctx.Value(uuidKey).(string)
requestUrl := fmt.Sprintf("%s%s", h.serviceConfig.sourceAppUri, uuid)
transactionId, _ := tid.GetTransactionIDFromContext(ctx)
Expand All @@ -53,12 +57,21 @@ func (h ContentHandler) getNativeContent(ctx context.Context, w http.ResponseWri
req.Header.Set("Authorization", "Basic "+h.serviceConfig.sourceAppAuth)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", "UPP Content Preview")
resp, err = client.Do(req)
resp, err := client.Do(req)

err = h.handleResponse(req, resp, err, w, uuid, h.serviceConfig.sourceAppName)
if err != nil {
if resp != nil {
// close now and don't return the resp
resp.Body.Close()
resp = nil
}
}

return h.handleResponse(req, resp, err, w, uuid, h.serviceConfig.sourceAppName)
return resp, err
}

func (h ContentHandler) getTransformedContent(ctx context.Context, nativeContentSourceAppResponse http.Response, w http.ResponseWriter) (bool, *http.Response) {
func (h ContentHandler) getTransformedContent(ctx context.Context, nativeContentSourceAppResponse http.Response, w http.ResponseWriter) (*http.Response, error) {
uuid := ctx.Value(uuidKey).(string)
requestUrl := fmt.Sprintf("%s?preview=true", h.serviceConfig.transformAppUri)
transactionId, _ := tid.GetTransactionIDFromContext(ctx)
Expand All @@ -73,25 +86,36 @@ func (h ContentHandler) getTransformedContent(ctx context.Context, nativeContent
req.Header.Set("User-Agent", "UPP Content Preview")
resp, err := client.Do(req)

return h.handleResponse(req, resp, err, w, uuid, h.serviceConfig.transformAppName)
err = h.handleResponse(req, resp, err, w, uuid, h.serviceConfig.transformAppName)
if err != nil {
if resp != nil {
// close now and don't return the resp
resp.Body.Close()
resp = nil
}
}

return resp, err
}

func (h ContentHandler) handleResponse(req *http.Request, extResp *http.Response, err error, w http.ResponseWriter, uuid, calledServiceName string) (bool, *http.Response) {
func (h ContentHandler) handleResponse(req *http.Request, extResp *http.Response, err error, w http.ResponseWriter, uuid, calledServiceName string) error {
//this happens when hostname cannot be resolved or host is not accessible
if err != nil {
h.handleError(w, err, calledServiceName, req.URL.String(), req.Header.Get(tid.TransactionIDHeader), uuid)
return false, nil
return err
}
switch extResp.StatusCode {
case http.StatusOK:
h.log.ResponseEvent(calledServiceName, req.URL.String(), extResp, uuid)
return true, extResp
return nil
case http.StatusUnprocessableEntity:
fallthrough
case http.StatusNotFound:
h.handleNotFound(w, extResp, calledServiceName, req.URL.String(), uuid)
return false, nil
h.handleClientError(w, calledServiceName, req.URL.String(), extResp, uuid)
return errors.New("not found")
default:
h.handleFailedRequest(w, extResp, calledServiceName, req.URL.String(), uuid)
return false, nil
h.handleFailedRequest(w, calledServiceName, req.URL.String(), extResp, uuid)
return errors.New("request failed")
}
}

Expand All @@ -101,14 +125,28 @@ func (h ContentHandler) handleError(w http.ResponseWriter, err error, serviceNam
h.metrics.recordErrorEvent()
}

func (h ContentHandler) handleFailedRequest(w http.ResponseWriter, resp *http.Response, serviceName string, url string, uuid string) {
func (h ContentHandler) handleFailedRequest(w http.ResponseWriter, serviceName string, url string, resp *http.Response, uuid string) {
w.WriteHeader(http.StatusServiceUnavailable)
h.log.RequestFailedEvent(serviceName, url, resp, uuid)
h.metrics.recordRequestFailedEvent()
}

func (h ContentHandler) handleNotFound(w http.ResponseWriter, resp *http.Response, serviceName string, url string, uuid string) {
w.WriteHeader(http.StatusNotFound)
func (h ContentHandler) handleClientError(w http.ResponseWriter, serviceName string, url string, resp *http.Response, uuid string) {
status := resp.StatusCode
w.WriteHeader(status)

msg := make(map[string]string)
switch status {
case http.StatusUnprocessableEntity:
msg["message"] = "Unable to map content for preview."
case http.StatusNotFound:
msg["message"] = "Content not found."
default:
msg["message"] = fmt.Sprintf("Unexpected error, call to %s returned HTTP status %v.", serviceName, status)
}
by, _ := json.Marshal(msg)
w.Write(by)

h.log.RequestFailedEvent(serviceName, url, resp, uuid)
h.metrics.recordRequestFailedEvent()
}

0 comments on commit 38ded06

Please sign in to comment.