Repository: goadesign/goa
Pull request #2830 (2021-05-05 03:49:41)

Add a DSL Redirect
This pull request adds a new DSL to define a HTTP redirect.

The DSL can be used like below:

```go
var _ = Service("service", func() {
	Method("method", func() {
		HTTP(func() {
			GET("/resources")
			Redirect("/redirect/dest", StatusMovedPermanently)
		})
	})
})

var _ = Service("service", func() {
	Files("/file.json", "/path/to/file.json", func() {
		Redirect("/redirect/dest", StatusMovedPermanently)
	})
})
```

The generated server handler initalizer for the HTTP endpoint redirect is:

```go
func NewMethodHandler(
	endpoint goa.Endpoint,
	mux goahttp.Muxer,
	decoder func(*http.Request) goahttp.Decoder,
	encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,
	errhandler func(context.Context, http.ResponseWriter, error),
	formatter func(err error) goahttp.Statuser,
) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept"))
		ctx = context.WithValue(ctx, goa.MethodKey, "Method")
		ctx = context.WithValue(ctx, goa.ServiceKey, "Service")
		http.Redirect(w, r, "/redirect/dest", http.StatusMovedPermanently)
	})
}
```

and the generated server mounter for the file server redirect is:

```go
func Mount(mux goahttp.Muxer) {
	MountPathToFileJSON(mux, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		http.Redirect(w, r, "/redirect/dest", http.StatusMovedPermanently)
	}))
}
```



Patch:
diff --git a/dsl/http_redirect.go b/dsl/http_redirect.go
new file mode 100644
index 00000000..f88d658b
--- /dev/null
+++ b/dsl/http_redirect.go
@@ -0,0 +1,49 @@
+package dsl
+
+import (
+	"goa.design/goa/v3/eval"
+	"goa.design/goa/v3/expr"
+)
+
+// Redirect indicates that HTTP requests reply to the request with a redirect.
+// The logic is the same as the standard http package Redirect function.
+//
+// Redirect must appear in a HTTP endpoint expression or a HTTP file server
+// expression.
+//
+// Redirect accepts 2 arguments. The first argument is the URL that is being
+// redirected to. The second argument is the HTTP status code.
+//
+// Example:
+//
+//    var _ = Service("service", func() {
+//        Method("method", func() {
+//            HTTP(func() {
+//                GET("/resources")
+//                Redirect("/redirect/dest", StatusMovedPermanently)
+//            })
+//        })
+//    })
+//
+//    var _ = Service("service", func() {
+//        Files("/file.json", "/path/to/file.json", func() {
+//            Redirect("/redirect/dest", StatusMovedPermanently)
+//        })
+//    })
+//
+func Redirect(url string, code int) {
+	redirect := &expr.HTTPRedirectExpr{
+		URL:        url,
+		StatusCode: code,
+	}
+	switch actual := eval.Current().(type) {
+	case *expr.HTTPEndpointExpr:
+		redirect.Parent = actual
+		actual.Redirect = redirect
+	case *expr.HTTPFileServerExpr:
+		redirect.Parent = actual
+		actual.Redirect = redirect
+	default:
+		eval.IncompatibleDSL()
+	}
+}
diff --git a/dsl/result_type.go b/dsl/result_type.go
index b0d52963..fafb1b84 100644
--- a/dsl/result_type.go
+++ b/dsl/result_type.go
@@ -278,6 +278,23 @@ func View(name string, adsl ...func()) {
 //         View("tiny")  // use "tiny" view to render the collection elements
 //     })
 //
+//     var MultiResultsExample = CollectionOf(DivisionResult, func() {
+//         Attributes(func() {
+//             Example("DivisionResult Collection Examples", func() {
+//                 Value([]Val{
+//                     {
+//                         "value":    4.167,
+//                         "reminder": 0,
+//                     },
+//                     {
+//                         "value":    3.0,
+//                         "reminder": 0,
+//                     },
+//                 })
+//             })
+//         })
+//     })
+//
 func CollectionOf(v interface{}, adsl ...func()) *expr.ResultTypeExpr {
 	var m *expr.ResultTypeExpr
 	var ok bool
diff --git a/expr/http_endpoint.go b/expr/http_endpoint.go
index 77ffefd1..cdc471aa 100644
--- a/expr/http_endpoint.go
+++ b/expr/http_endpoint.go
@@ -64,6 +64,8 @@ type (
 		// MultipartRequest indicates that the request content type for
 		// the endpoint is a multipart type.
 		MultipartRequest bool
+		// Redirect defines a redirect for the endpoint.
+		Redirect *HTTPRedirectExpr
 		// Meta is a set of key/value pairs with semantic that is
 		// specific to each generator, see dsl.Meta.
 		Meta MetaExpr
@@ -261,7 +263,9 @@ func (e *HTTPEndpointExpr) Prepare() {
 	// Make sure there's a default success response if none define explicitly.
 	if len(e.Responses) == 0 {
 		status := StatusOK
-		if e.MethodExpr.Result.Type == Empty && !e.SkipResponseBodyEncodeDecode {
+		if e.Redirect != nil {
+			status = e.Redirect.StatusCode
+		} else if e.MethodExpr.Result.Type == Empty && !e.SkipResponseBodyEncodeDecode {
 			status = StatusNoContent
 		}
 		e.Responses = []*HTTPResponseExpr{{StatusCode: status}}
@@ -342,6 +346,20 @@ func (e *HTTPEndpointExpr) Validate() error {
 		}
 	}
 
+	// Redirect is not compatible with Response.
+	if e.Redirect != nil {
+		found := false
+		for _, r := range e.Responses {
+			if r.StatusCode != e.Redirect.StatusCode {
+				found = true
+				break
+			}
+		}
+		if found {
+			verr.Add(e, "Endpoint cannot use Response when using Redirect.")
+		}
+	}
+
 	// Validate routes
 
 	// Routes cannot be empty
diff --git a/expr/http_file_server.go b/expr/http_file_server.go
index ba234ce8..f7d90891 100644
--- a/expr/http_file_server.go
+++ b/expr/http_file_server.go
@@ -20,6 +20,8 @@ type (
 		FilePath string
 		// RequestPaths is the list of HTTP paths that serve the assets.
 		RequestPaths []string
+		// Redirect defines a redirect for the endpoint.
+		Redirect *HTTPRedirectExpr
 		// Meta is a list of key/value pairs
 		Meta MetaExpr
 	}
diff --git a/expr/http_redirect.go b/expr/http_redirect.go
new file mode 100644
index 00000000..d76abea3
--- /dev/null
+++ b/expr/http_redirect.go
@@ -0,0 +1,29 @@
+package expr
+
+import (
+	"fmt"
+
+	"goa.design/goa/v3/eval"
+)
+
+type (
+	// HTTPRedirectExpr defines an endpoint that replies to the request with a redirect.
+	HTTPRedirectExpr struct {
+		// URL is the URL that is being redirected to.
+		URL string
+		// StatusCode is the HTTP status code.
+		StatusCode int
+		// Parent expression, one of HTTPEndpointExpr or HTTPFileServerExpr.
+		Parent eval.Expression
+	}
+)
+
+// EvalName returns the generic definition name used in error messages.
+func (r *HTTPRedirectExpr) EvalName() string {
+	suffix := fmt.Sprintf("redirect to %s with status code %d", r.URL, r.StatusCode)
+	var prefix string
+	if r.Parent != nil {
+		prefix = r.Parent.EvalName() + " "
+	}
+	return prefix + suffix
+}
diff --git a/http/codegen/server.go b/http/codegen/server.go
index c76ae87f..969e9612 100644
--- a/http/codegen/server.go
+++ b/http/codegen/server.go
@@ -113,7 +113,7 @@ func serverEncodeDecodeFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.F
 	}
 
 	for _, e := range data.Endpoints {
-		if !isWebSocketEndpoint(e) {
+		if e.Redirect == nil && !isWebSocketEndpoint(e) {
 			sections = append(sections, &codegen.SectionTemplate{
 				Name:    "response-encoder",
 				FuncMap: transTmplFuncs(svc),
@@ -342,7 +342,11 @@ func {{ .MountServer }}(mux goahttp.Muxer{{ if .Endpoints }}, h *{{ .ServerStruc
 	{{ .MountHandler }}(mux, h.{{ .Method.VarName }})
 	{{- end }}
 	{{- range .FileServers }}
-		{{- if .IsDir }}
+		{{- if .Redirect }}
+	{{ .MountHandler }}(mux, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+			http.Redirect(w, r, "{{ .Redirect.URL }}", {{ .Redirect.StatusCode }})
+		}))
+		{{- else if .IsDir }}
 	{{ .MountHandler }}(mux, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 			upath := path.Clean(r.URL.Path)
 			rpath := upath
@@ -407,29 +411,35 @@ func {{ .HandlerInit }}(
 	configurer goahttp.ConnConfigureFunc,
 	{{- end }}
 ) http.Handler {
+	{{- if (or (mustDecodeRequest .) (not (or .Redirect (isWebSocketEndpoint .))) (not .Redirect) .Method.SkipResponseBodyEncodeDecode) }}
 	var (
+	{{- end }}
 		{{- if mustDecodeRequest . }}
 		decodeRequest  = {{ .RequestDecoder }}(mux, decoder)
 		{{- end }}
-		{{- if not (isWebSocketEndpoint .) }}
+		{{- if not (or .Redirect (isWebSocketEndpoint .)) }}
 		encodeResponse = {{ .ResponseEncoder }}(encoder)
 		{{- end }}
+		{{- if (or (mustDecodeRequest .) (not .Redirect) .Method.SkipResponseBodyEncodeDecode) }}
 		encodeError    = {{ if .Errors }}{{ .ErrorEncoder }}{{ else }}goahttp.ErrorEncoder{{ end }}(encoder, formatter)
+		{{- end }}
+	{{- if (or (mustDecodeRequest .) (not (or .Redirect (isWebSocketEndpoint .))) (not .Redirect) .Method.SkipResponseBodyEncodeDecode) }}
 	)
+	{{- end }}
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
 		ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept"))
 		ctx = context.WithValue(ctx, goa.MethodKey, {{ printf "%q" .Method.Name }})
 		ctx = context.WithValue(ctx, goa.ServiceKey, {{ printf "%q" .ServiceName }})
 
 	{{- if mustDecodeRequest . }}
-		payload, err := decodeRequest(r)
+		{{ if .Redirect }}_{{ else }}payload{{ end }}, err := decodeRequest(r)
 		if err != nil {
 			if err := encodeError(ctx, w, err); err != nil {
 				errhandler(ctx, w, err)
 			}
 			return
 		}
-	{{- else }}
+	{{- else if not .Redirect }}
 		var err error
 	{{- end }}
 	{{- if isWebSocketEndpoint . }}
@@ -451,9 +461,12 @@ func {{ .HandlerInit }}(
 	{{- else if .Method.SkipRequestBodyEncodeDecode }}
 		data := &{{ .ServicePkgName }}.{{ .Method.RequestStruct }}{ {{ if .Payload.Ref }}Payload: payload.({{ .Payload.Ref }}), {{ end }}Body: r.Body }
 		res, err := endpoint(ctx, data)
+	{{- else if .Redirect }}
+		http.Redirect(w, r, "{{ .Redirect.URL }}", {{ .Redirect.StatusCode }})
 	{{- else }}
 		res, err := endpoint(ctx, {{ if .Payload.Ref }}payload{{ else }}nil{{ end }})
 	{{- end }}
+	{{- if not .Redirect }}
 		if err != nil {
 			{{- if isWebSocketEndpoint . }}
 			if _, ok := err.(websocket.HandshakeError); ok {
@@ -465,11 +478,12 @@ func {{ .HandlerInit }}(
 			}
 			return
 		}
+	{{- end }}
 	{{- if .Method.SkipResponseBodyEncodeDecode }}
 		o := res.(*{{ .ServicePkgName }}.{{ .Method.ResponseStruct }})
 		defer o.Body.Close()
 	{{- end }}
-	{{- if not (isWebSocketEndpoint .) }}
+	{{- if not (or .Redirect (isWebSocketEndpoint .)) }}
 		if err := encodeResponse(ctx, w, {{ if and .Method.SkipResponseBodyEncodeDecode .Result.Ref }}o.Result{{ else }}res{{ end }}); err != nil {
 			errhandler(ctx, w, err)
 			{{- if .Method.SkipResponseBodyEncodeDecode }}
diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go
index 413f96de..f873f344 100644
--- a/http/codegen/service_data.go
+++ b/http/codegen/service_data.go
@@ -144,6 +144,8 @@ type (
 		// ServerWebSocket holds the data to render the server struct which
 		// implements the server stream interface.
 		ServerWebSocket *WebSocketData
+		// Redirect defines a redirect for the endpoint.
+		Redirect *RedirectData
 
 		// client
 
@@ -183,6 +185,16 @@ type (
 		// PathParam is the name of the parameter used to capture the
 		// path for file servers that serve files under a directory.
 		PathParam string
+		// Redirect defines a redirect for the endpoint.
+		Redirect *RedirectData
+	}
+
+	// RedirectData lists the data needed to generate a redirect.
+	RedirectData struct {
+		// URL is the URL that is being redirected to.
+		URL string
+		// StatusCode is the HTTP status code.
+		StatusCode string
 	}
 
 	// PayloadData contains the payload information required to generate the
@@ -595,12 +607,20 @@ func (d ServicesData) analyze(hs *expr.HTTPServiceExpr) *ServiceData {
 		if s.IsDir() {
 			pp = expr.ExtractHTTPWildcards(s.RequestPaths[0])[0]
 		}
+		var redirect *RedirectData
+		if s.Redirect != nil {
+			redirect = &RedirectData{
+				URL:        s.Redirect.URL,
+				StatusCode: statusCodeToHTTPConst(s.Redirect.StatusCode),
+			}
+		}
 		data := &FileServerData{
 			MountHandler: scope.Unique(fmt.Sprintf("Mount%s", codegen.Goify(s.FilePath, true))),
 			RequestPaths: paths,
 			FilePath:     s.FilePath,
 			IsDir:        s.IsDir(),
 			PathParam:    pp,
+			Redirect:     redirect,
 		}
 		rd.FileServers = append(rd.FileServers, data)
 	}
@@ -825,6 +845,13 @@ func (d ServicesData) analyze(hs *expr.HTTPServiceExpr) *ServiceData {
 			ad.BuildStreamPayload = scope.Unique("Build" + codegen.Goify(ep.Name, true) + "StreamPayload")
 		}
 
+		if a.Redirect != nil {
+			ad.Redirect = &RedirectData{
+				URL:        a.Redirect.URL,
+				StatusCode: statusCodeToHTTPConst(a.Redirect.StatusCode),
+			}
+		}
+
 		rd.Endpoints = append(rd.Endpoints, ad)
 	}
 


Test patch:
diff --git a/http/codegen/handler_test.go b/http/codegen/handler_test.go
index 942bf2b1..7584647e 100644
--- a/http/codegen/handler_test.go
+++ b/http/codegen/handler_test.go
@@ -11,22 +11,25 @@ import (
 func TestHandlerInit(t *testing.T) {
 	const genpkg = "gen"
 	cases := []struct {
-		Name string
-		DSL  func()
-		Code string
+		Name      string
+		DSL       func()
+		Code      string
+		FileCount int
 	}{
-		{"no payload no result", testdata.ServerNoPayloadNoResultDSL, testdata.ServerNoPayloadNoResultHandlerConstructorCode},
-		{"payload no result", testdata.ServerPayloadNoResultDSL, testdata.ServerPayloadNoResultHandlerConstructorCode},
-		{"no payload result", testdata.ServerNoPayloadResultDSL, testdata.ServerNoPayloadResultHandlerConstructorCode},
-		{"payload result", testdata.ServerPayloadResultDSL, testdata.ServerPayloadResultHandlerConstructorCode},
-		{"payload result error", testdata.ServerPayloadResultErrorDSL, testdata.ServerPayloadResultErrorHandlerConstructorCode},
+		{"no payload no result", testdata.ServerNoPayloadNoResultDSL, testdata.ServerNoPayloadNoResultHandlerConstructorCode, 2},
+		{"no payload no result with a redirect", testdata.ServerNoPayloadNoResultWithRedirectDSL, testdata.ServerNoPayloadNoResultWithRedirectHandlerConstructorCode, 1},
+		{"payload no result", testdata.ServerPayloadNoResultDSL, testdata.ServerPayloadNoResultHandlerConstructorCode, 2},
+		{"payload no result with a redirect", testdata.ServerPayloadNoResultWithRedirectDSL, testdata.ServerPayloadNoResultWithRedirectHandlerConstructorCode, 2},
+		{"no payload result", testdata.ServerNoPayloadResultDSL, testdata.ServerNoPayloadResultHandlerConstructorCode, 2},
+		{"payload result", testdata.ServerPayloadResultDSL, testdata.ServerPayloadResultHandlerConstructorCode, 2},
+		{"payload result error", testdata.ServerPayloadResultErrorDSL, testdata.ServerPayloadResultErrorHandlerConstructorCode, 2},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
 			RunHTTPDSL(t, c.DSL)
 			fs := ServerFiles(genpkg, expr.Root)
-			if len(fs) != 2 {
-				t.Fatalf("got %d files, expected two", len(fs))
+			if len(fs) != c.FileCount {
+				t.Fatalf("got %d files, expected %d", len(fs), c.FileCount)
 			}
 			sections := fs[0].SectionTemplates
 			if len(sections) < 7 {
diff --git a/http/codegen/server_handler_test.go b/http/codegen/server_handler_test.go
index 2df6ca19..d838fb45 100644
--- a/http/codegen/server_handler_test.go
+++ b/http/codegen/server_handler_test.go
@@ -14,17 +14,19 @@ func TestServerHandler(t *testing.T) {
 		Name       string
 		DSL        func()
 		Code       string
+		FileCount  int
 		SectionNum int
 	}{
-		{"server simple routing", testdata.ServerSimpleRoutingDSL, testdata.ServerSimpleRoutingCode, 7},
-		{"server trailing slash routing", testdata.ServerTrailingSlashRoutingDSL, testdata.ServerTrailingSlashRoutingCode, 7},
+		{"server simple routing", testdata.ServerSimpleRoutingDSL, testdata.ServerSimpleRoutingCode, 2, 7},
+		{"server trailing slash routing", testdata.ServerTrailingSlashRoutingDSL, testdata.ServerTrailingSlashRoutingCode, 2, 7},
+		{"server simple routing with a redirect", testdata.ServerSimpleRoutingWithRedirectDSL, testdata.ServerSimpleRoutingCode, 1, 7},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
 			RunHTTPDSL(t, c.DSL)
 			fs := ServerFiles(genpkg, expr.Root)
-			if len(fs) != 2 {
-				t.Fatalf("got %d files, expected 1", len(fs))
+			if len(fs) != c.FileCount {
+				t.Fatalf("got %d files, expected %d", len(fs), c.FileCount)
 			}
 			sections := fs[0].SectionTemplates
 			if len(sections) < 8 {
diff --git a/http/codegen/server_init_test.go b/http/codegen/server_init_test.go
index 6677daa0..f4a207cb 100644
--- a/http/codegen/server_init_test.go
+++ b/http/codegen/server_init_test.go
@@ -20,6 +20,7 @@ func TestServerInit(t *testing.T) {
 		{"multiple endpoints", testdata.ServerMultiEndpointsDSL, testdata.ServerMultiEndpointsConstructorCode, 2, 3},
 		{"multiple bases", testdata.ServerMultiBasesDSL, testdata.ServerMultiBasesConstructorCode, 2, 3},
 		{"file server", testdata.ServerFileServerDSL, testdata.ServerFileServerConstructorCode, 1, 3},
+		{"file server with a redirect", testdata.ServerFileServerWithRedirectDSL, testdata.ServerFileServerConstructorCode, 1, 3},
 		{"mixed", testdata.ServerMixedDSL, testdata.ServerMixedConstructorCode, 2, 3},
 		{"multipart", testdata.ServerMultipartDSL, testdata.ServerMultipartConstructorCode, 2, 4},
 		{"streaming", testdata.StreamingResultDSL, testdata.ServerStreamingConstructorCode, 3, 3},
diff --git a/http/codegen/server_mount_test.go b/http/codegen/server_mount_test.go
index 656ec395..c6b75691 100644
--- a/http/codegen/server_mount_test.go
+++ b/http/codegen/server_mount_test.go
@@ -14,19 +14,24 @@ func TestServerMount(t *testing.T) {
 		Name       string
 		DSL        func()
 		Code       string
+		FileCount  int
 		SectionNum int
 	}{
-		{"multiple files constructor", testdata.ServerMultipleFilesDSL, testdata.ServerMultipleFilesConstructorCode, 6},
-		{"multiple files mounter", testdata.ServerMultipleFilesDSL, testdata.ServerMultipleFilesMounterCode, 9},
-		{"multiple files constructor /w prefix path", testdata.ServerMultipleFilesWithPrefixPathDSL, testdata.ServerMultipleFilesWithPrefixPathConstructorCode, 6},
-		{"multiple files mounter /w prefix path", testdata.ServerMultipleFilesWithPrefixPathDSL, testdata.ServerMultipleFilesWithPrefixPathMounterCode, 9},
+		{"simple routing constructor", testdata.ServerSimpleRoutingDSL, testdata.ServerSimpleRoutingConstructorCode, 2, 6},
+		{"simple routing with a redirect constructor", testdata.ServerSimpleRoutingWithRedirectDSL, testdata.ServerSimpleRoutingConstructorCode, 1, 6},
+		{"multiple files constructor", testdata.ServerMultipleFilesDSL, testdata.ServerMultipleFilesConstructorCode, 1, 6},
+		{"multiple files mounter", testdata.ServerMultipleFilesDSL, testdata.ServerMultipleFilesMounterCode, 1, 9},
+		{"multiple files constructor /w prefix path", testdata.ServerMultipleFilesWithPrefixPathDSL, testdata.ServerMultipleFilesWithPrefixPathConstructorCode, 1, 6},
+		{"multiple files mounter /w prefix path", testdata.ServerMultipleFilesWithPrefixPathDSL, testdata.ServerMultipleFilesWithPrefixPathMounterCode, 1, 9},
+		{"multiple files with a redirect constructor", testdata.ServerMultipleFilesWithRedirectDSL, testdata.ServerMultipleFilesWithRedirectConstructorCode, 1, 6},
+		{"multiple files with a redirect mounter", testdata.ServerMultipleFilesWithRedirectDSL, testdata.ServerMultipleFilesMounterCode, 1, 9},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
 			RunHTTPDSL(t, c.DSL)
 			fs := ServerFiles(genpkg, expr.Root)
-			if len(fs) != 1 {
-				t.Fatalf("got %d files, expected 1", len(fs))
+			if len(fs) != c.FileCount {
+				t.Fatalf("got %d files, expected %d", len(fs), c.FileCount)
 			}
 			sections := fs[0].SectionTemplates
 			if len(sections) < 6 {
diff --git a/http/codegen/testdata/handler_init_functions.go b/http/codegen/testdata/handler_init_functions.go
index 9712f257..01027aa8 100644
--- a/http/codegen/testdata/handler_init_functions.go
+++ b/http/codegen/testdata/handler_init_functions.go
@@ -34,6 +34,26 @@ func NewMethodNoPayloadNoResultHandler(
 }
 `
 
+var ServerNoPayloadNoResultWithRedirectHandlerConstructorCode = `// NewMethodNoPayloadNoResultHandler creates a HTTP handler which loads the
+// HTTP request and calls the "ServiceNoPayloadNoResult" service
+// "MethodNoPayloadNoResult" endpoint.
+func NewMethodNoPayloadNoResultHandler(
+	endpoint goa.Endpoint,
+	mux goahttp.Muxer,
+	decoder func(*http.Request) goahttp.Decoder,
+	encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,
+	errhandler func(context.Context, http.ResponseWriter, error),
+	formatter func(err error) goahttp.Statuser,
+) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept"))
+		ctx = context.WithValue(ctx, goa.MethodKey, "MethodNoPayloadNoResult")
+		ctx = context.WithValue(ctx, goa.ServiceKey, "ServiceNoPayloadNoResult")
+		http.Redirect(w, r, "/redirect/dest", http.StatusMovedPermanently)
+	})
+}
+`
+
 var ServerPayloadNoResultHandlerConstructorCode = `// NewMethodPayloadNoResultHandler creates a HTTP handler which loads the HTTP
 // request and calls the "ServicePayloadNoResult" service
 // "MethodPayloadNoResult" endpoint.
@@ -75,6 +95,37 @@ func NewMethodPayloadNoResultHandler(
 }
 `
 
+var ServerPayloadNoResultWithRedirectHandlerConstructorCode = `// NewMethodPayloadNoResultHandler creates a HTTP handler which loads the HTTP
+// request and calls the "ServicePayloadNoResult" service
+// "MethodPayloadNoResult" endpoint.
+func NewMethodPayloadNoResultHandler(
+	endpoint goa.Endpoint,
+	mux goahttp.Muxer,
+	decoder func(*http.Request) goahttp.Decoder,
+	encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,
+	errhandler func(context.Context, http.ResponseWriter, error),
+	formatter func(err error) goahttp.Statuser,
+) http.Handler {
+	var (
+		decodeRequest = DecodeMethodPayloadNoResultRequest(mux, decoder)
+		encodeError   = goahttp.ErrorEncoder(encoder, formatter)
+	)
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept"))
+		ctx = context.WithValue(ctx, goa.MethodKey, "MethodPayloadNoResult")
+		ctx = context.WithValue(ctx, goa.ServiceKey, "ServicePayloadNoResult")
+		_, err := decodeRequest(r)
+		if err != nil {
+			if err := encodeError(ctx, w, err); err != nil {
+				errhandler(ctx, w, err)
+			}
+			return
+		}
+		http.Redirect(w, r, "/redirect/dest", http.StatusMovedPermanently)
+	})
+}
+`
+
 var ServerNoPayloadResultHandlerConstructorCode = `// NewMethodNoPayloadResultHandler creates a HTTP handler which loads the HTTP
 // request and calls the "ServiceNoPayloadResult" service
 // "MethodNoPayloadResult" endpoint.
diff --git a/http/codegen/testdata/server_dsls.go b/http/codegen/testdata/server_dsls.go
index b103c0a4..bbc22c01 100644
--- a/http/codegen/testdata/server_dsls.go
+++ b/http/codegen/testdata/server_dsls.go
@@ -14,6 +14,17 @@ var ServerNoPayloadNoResultDSL = func() {
 	})
 }
 
+var ServerNoPayloadNoResultWithRedirectDSL = func() {
+	Service("ServiceNoPayloadNoResult", func() {
+		Method("MethodNoPayloadNoResult", func() {
+			HTTP(func() {
+				POST("/")
+				Redirect("/redirect/dest", StatusMovedPermanently)
+			})
+		})
+	})
+}
+
 var ServerPayloadNoResultDSL = func() {
 	Service("ServicePayloadNoResult", func() {
 		Method("MethodPayloadNoResult", func() {
@@ -27,6 +38,20 @@ var ServerPayloadNoResultDSL = func() {
 	})
 }
 
+var ServerPayloadNoResultWithRedirectDSL = func() {
+	Service("ServicePayloadNoResult", func() {
+		Method("MethodPayloadNoResult", func() {
+			Payload(func() {
+				Attribute("a", Boolean)
+			})
+			HTTP(func() {
+				POST("/")
+				Redirect("/redirect/dest", StatusMovedPermanently)
+			})
+		})
+	})
+}
+
 var ServerNoPayloadResultDSL = func() {
 	Service("ServiceNoPayloadResult", func() {
 		Method("MethodNoPayloadResult", func() {
@@ -130,18 +155,42 @@ var ServerFileServerDSL = func() {
 	})
 }
 
+var ServerFileServerWithRedirectDSL = func() {
+	Service("ServiceFileServer", func() {
+		HTTP(func() {
+			Path("/server_file_server")
+		})
+		Files("/file1.json", "/path/to/file1.json", func() {
+			Redirect("/redirect/dest", StatusMovedPermanently)
+		})
+		Files("/file2.json", "/path/to/file2.json")
+		Files("/file3.json", "/path/to/file3.json")
+	})
+}
+
 var ServerMixedDSL = func() {
 	Service("ServerMixed", func() {
-		Method("MethodMixed", func() {
+		Method("MethodMixed1", func() {
 			Payload(func() {
 				Attribute("id", String)
 			})
 			HTTP(func() {
-				GET("/{id}")
+				GET("/resources1/{id}")
+			})
+		})
+		Method("MethodMixed2", func() {
+			Payload(func() {
+				Attribute("id", String)
+			})
+			HTTP(func() {
+				GET("/resources2/{id}")
+				Redirect("/redirect/dest1", StatusMovedPermanently)
 			})
 		})
 		Files("/file1.json", "/path/to/file1.json")
-		Files("/file2.json", "/path/to/file2.json")
+		Files("/file2.json", "/path/to/file2.json", func() {
+			Redirect("/redirect/dest2", StatusMovedPermanently)
+		})
 	})
 }
 
@@ -176,6 +225,16 @@ var ServerMultipleFilesWithPrefixPathDSL = func() {
 	})
 }
 
+var ServerMultipleFilesWithRedirectDSL = func() {
+	Service("ServiceFileServer", func() {
+		Files("/file.json", "/path/to/file.json", func() {
+			Redirect("/redirect/dest", StatusMovedPermanently)
+		})
+		Files("/", "/path/to/file.json")
+		Files("/{wildcard}", "/path/to/folder")
+	})
+}
+
 var ServerSimpleRoutingDSL = func() {
 	Service("ServiceSimpleRoutingServer", func() {
 		Method("server-simple-routing", func() {
@@ -195,3 +254,14 @@ var ServerTrailingSlashRoutingDSL = func() {
 		})
 	})
 }
+
+var ServerSimpleRoutingWithRedirectDSL = func() {
+	Service("ServiceSimpleRoutingServer", func() {
+		Method("server-simple-routing", func() {
+			HTTP(func() {
+				GET("/simple/routing")
+				Redirect("/redirect/dest", StatusMovedPermanently)
+			})
+		})
+	})
+}
diff --git a/http/codegen/testdata/server_init_functions.go b/http/codegen/testdata/server_init_functions.go
index 7802d4fb..e1a7a35a 100644
--- a/http/codegen/testdata/server_init_functions.go
+++ b/http/codegen/testdata/server_init_functions.go
@@ -89,11 +89,13 @@ func New(
 ) *Server {
 	return &Server{
 		Mounts: []*MountPoint{
-			{"MethodMixed", "GET", "/{id}"},
+			{"MethodMixed1", "GET", "/resources1/{id}"},
+			{"MethodMixed2", "GET", "/resources2/{id}"},
 			{"/path/to/file1.json", "GET", "/file1.json"},
 			{"/path/to/file2.json", "GET", "/file2.json"},
 		},
-		MethodMixed: NewMethodMixedHandler(e.MethodMixed, mux, decoder, encoder, errhandler, formatter),
+		MethodMixed1: NewMethodMixed1Handler(e.MethodMixed1, mux, decoder, encoder, errhandler, formatter),
+		MethodMixed2: NewMethodMixed2Handler(e.MethodMixed2, mux, decoder, encoder, errhandler, formatter),
 	}
 }
 `
@@ -185,6 +187,28 @@ func Mount(mux goahttp.Muxer) {
 }
 `
 
+var ServerMultipleFilesWithRedirectConstructorCode = `// Mount configures the mux to serve the ServiceFileServer endpoints.
+func Mount(mux goahttp.Muxer) {
+	MountPathToFileJSON(mux, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		http.Redirect(w, r, "/redirect/dest", http.StatusMovedPermanently)
+	}))
+	MountPathToFileJSON2(mux, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		http.ServeFile(w, r, "/path/to/file.json")
+	}))
+	MountPathToFolder(mux, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		upath := path.Clean(r.URL.Path)
+		rpath := upath
+		http.ServeFile(w, r, path.Join("/path/to/folder", rpath))
+	}))
+}
+`
+
+var ServerSimpleRoutingConstructorCode = `// Mount configures the mux to serve the ServiceSimpleRoutingServer endpoints.
+func Mount(mux goahttp.Muxer, h *Server) {
+	MountServerSimpleRoutingHandler(mux, h.ServerSimpleRouting)
+}
+`
+
 var ServerMultipleFilesMounterCode = `// MountPathToFolder configures the mux to serve GET request made to "/".
 func MountPathToFolder(mux goahttp.Muxer, h http.Handler) {
 	mux.Handle("GET", "/", h.ServeHTTP)