Repository: goadesign/goa
Pull request #3690 (2025-04-02 16:23:32)

fix(codegen): dedupe security schemes by type
This commit solves a bug where designs that use multiple security schemes _of the same type_ break the generated `Auther` interface for a service as well as endpoint-related init methods due to unnecessary code duplication from ranging over `.Schemes`.

## Reproducer

```go
import (
	. "goa.design/goa/v3/dsl"
)

var AcmeAPIKey = APIKeySecurity("acme_api_key")
var AcmeTenant = APIKeySecurity("acme_tenant_id")

var _ = Service("customers", func() {
	Security(AcmeAPIKey, AcmeTenant)

        // ...
})
```

This currently results in code like this:

```go
// Auther defines the authorization functions to be implemented by the service.
type Auther interface {
	// APIKeyAuth implements the authorization logic for the APIKey security scheme.
	APIKeyAuth(ctx context.Context, key string, schema *security.APIKeyScheme) (context.Context, error)
	// APIKeyAuth implements the authorization logic for the APIKey security scheme.
	APIKeyAuth(ctx context.Context, key string, schema *security.APIKeyScheme) (context.Context, error)
}
```

And:

```go
// NewEndpoints wraps the methods of the "assets" service with endpoints.
func NewEndpoints(s Service) *Endpoints {
	// Casting service to Auther interface
	a := s.(Auther)
	return &Endpoints{
		UploadOpenAPIv3: NewUpdateDetailsEndpoint(s, a.APIKeyAuth, a.APIKeyAuth),
		//                                                         ^ 🚨
	}
}

// ...

func NewUpdateDetailsEndpoint(s Service, authAPIKeyFn security.AuthAPIKeyFunc, authAPIKeyFn security.AuthAPIKeyFunc) goa.Endpoint {
//                                                                             ^ 🚨
	// ...
}
```

disintegrator: Just need to figure out testing but putting it up PR to start discussion.

Patch:
diff --git a/.gitignore b/.gitignore
index 2119f65e..5d07e62b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,4 +16,6 @@ cmd/goa/goa
 
 # DeepSource cruft
 cover.out
-bin/
+
+# MacOS cruft
+**/.DS_Store
diff --git a/.golangci.yml b/.golangci.yml
index c3283b47..6e138449 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,3 +1,7 @@
+version: "2"
 linters:
   enable:
     - errorlint
+    - errcheck
+    - staticcheck
+
diff --git a/Makefile b/Makefile
index 067e39e4..0a2bbcc4 100644
--- a/Makefile
+++ b/Makefile
@@ -13,8 +13,8 @@
 # - "all" is the default target, it runs "lint" and "test"
 #
 MAJOR=3
-MINOR=20
-BUILD=1
+MINOR=21
+BUILD=0
 
 GOOS=$(shell go env GOOS)
 GOARCH=$(shell go env GOARCH)
@@ -70,9 +70,7 @@ depend:
 
 lint:
 ifneq ($(GOOS),windows)
-	@if [ "`golangci-lint run ./... | grep -v ".pb.go" | tee /dev/stderr`" ]; then \
-		echo "^ - lint errors!" && echo && exit 1; \
-	fi
+	@golangci-lint run ./... || (echo "^ - lint errors!" && echo && exit 1)
 endif
 
 test:
diff --git a/README.md b/README.md
index 2f375c1e..c6838e23 100644
--- a/README.md
+++ b/README.md
@@ -6,7 +6,7 @@
   </p>
   <p align="center">
     <a href="https://github.com/goadesign/goa/releases/latest"><img alt="Release" src="https://img.shields.io/github/release/goadesign/goa.svg?style=for-the-badge"></a>
-    <a href="https://pkg.go.dev/goa.design/goa/v3@v3.20.1/dsl?tab=doc"><img alt="Go Doc" src="https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge"></a>
+    <a href="https://pkg.go.dev/goa.design/goa/v3@v3.21.0/dsl?tab=doc"><img alt="Go Doc" src="https://img.shields.io/badge/godoc-reference-blue.svg?style=for-the-badge"></a>
     <a href="https://github.com/goadesign/goa/actions/workflows/ci.yml"><img alt="GitHub Action: Test" src="https://img.shields.io/github/actions/workflow/status/goadesign/goa/test.yml?branch=v3&style=for-the-badge"></a>
     <a href="https://goreportcard.com/report/github.com/goadesign/goa"><img alt="Go Report Card" src="https://goreportcard.com/badge/github.com/goadesign/goa?style=for-the-badge"></a>
     <a href="/LICENSE"><img alt="Software License" src="https://img.shields.io/badge/license-MIT-brightgreen.svg?style=for-the-badge"></a>
@@ -74,7 +74,7 @@ The result? Dramatically reduced development time, consistent APIs, and the elim
     <tr>
         <td>
             <img width="1000" height="0" />
-            <a href="https://www.speakeasy.com/docs/api-frameworks/goa?utm_source=goa+repo&utm_medium=github+sponsorship">
+            <a href="https://www.speakeasy.com/editor?utm_source=goa+repo&utm_medium=github+sponsorship">
                 <img src="docs/speakeasy.png" alt="Speakeasy" width="260" align="right" />
             </a>
             <h3>Speakeasy: Enterprise DevEx for your API</h3>
diff --git a/cmd/goa/gen.go b/cmd/goa/gen.go
index 3a56485c..7b01a761 100644
--- a/cmd/goa/gen.go
+++ b/cmd/goa/gen.go
@@ -226,7 +226,7 @@ func (g *Generator) Run() ([]string, error) {
 // Remove deletes the package files.
 func (g *Generator) Remove() {
 	if g.tmpDir != "" {
-		os.RemoveAll(g.tmpDir)
+		_ = os.RemoveAll(g.tmpDir)
 		g.tmpDir = ""
 	}
 }
@@ -237,7 +237,7 @@ func (g *Generator) runGoCmd(args ...string) error {
 		return fmt.Errorf(`failed to find a go compiler, looked in "%s"`, os.Getenv("PATH"))
 	}
 	if g.DesignVersion > 2 {
-		os.Setenv("GO111MODULE", "on")
+		_ = os.Setenv("GO111MODULE", "on")
 	}
 	c := exec.Cmd{
 		Path: gobin,
diff --git a/codegen/file.go b/codegen/file.go
index 983aec10..62a9a046 100644
--- a/codegen/file.go
+++ b/codegen/file.go
@@ -165,7 +165,9 @@ func finalizeGoSource(path string) error {
 	if err := format.Node(w, fset, file); err != nil {
 		return err
 	}
-	w.Close()
+	if err := w.Close(); err != nil {
+		return err
+	}
 
 	// Format code using goimport standard
 	bs, err := os.ReadFile(path)
diff --git a/codegen/go_transform.go b/codegen/go_transform.go
index 821d477e..a4c5795b 100644
--- a/codegen/go_transform.go
+++ b/codegen/go_transform.go
@@ -197,7 +197,7 @@ func transformObject(source, target *expr.AttributeExpr, sourceVar, targetVar st
 		assign = ":="
 	}
 	name := ta.TargetCtx.Scope.Name(target, ta.TargetCtx.Pkg(target), ta.TargetCtx.Pointer, ta.TargetCtx.UseDefault)
-	buffer.WriteString(fmt.Sprintf("%s %s %s%s{%s}\n", targetVar, assign, deref, name, initCode))
+	fmt.Fprintf(buffer, "%s %s %s%s{%s}\n", targetVar, assign, deref, name, initCode)
 	buffer.WriteString(postInitCode)
 
 	// iterate through attributes to initialize rest of the struct fields and
diff --git a/codegen/service/service_data.go b/codegen/service/service_data.go
index c29d8631..e1cbb366 100644
--- a/codegen/service/service_data.go
+++ b/codegen/service/service_data.go
@@ -679,6 +679,21 @@ func (s SchemesData) Append(d *SchemeData) SchemesData {
 	return append(s, d)
 }
 
+// DedupeByType returns a new SchemesData slice that is deduplicated by scheme
+// type.
+func (s SchemesData) DedupeByType() SchemesData {
+	seen := make(map[string]struct{})
+	uniqueSchemes := SchemesData{}
+	for _, s := range s {
+		if _, ok := seen[s.Type]; !ok {
+			seen[s.Type] = struct{}{}
+			uniqueSchemes = append(uniqueSchemes, s)
+		}
+	}
+
+	return uniqueSchemes
+}
+
 // analyze creates the data necessary to render the code of the given service.
 // It records the user types needed by the service definition in userTypes.
 func (d ServicesData) analyze(service *expr.ServiceExpr) *Data {
@@ -1018,11 +1033,11 @@ func collectUnionMethods(att *expr.AttributeExpr, scope *codegen.NameScope, loc
 
 // buildErrorInitData creates the data needed to generate code around endpoint error return values.
 func buildErrorInitData(er *expr.ErrorExpr, scope *codegen.NameScope) *ErrorInitData {
-	_, temporary := er.AttributeExpr.Meta["goa:error:temporary"]
-	_, timeout := er.AttributeExpr.Meta["goa:error:timeout"]
-	_, fault := er.AttributeExpr.Meta["goa:error:fault"]
+	_, temporary := er.Meta["goa:error:temporary"]
+	_, timeout := er.Meta["goa:error:timeout"]
+	_, fault := er.Meta["goa:error:fault"]
 	var pkg string
-	if ut, ok := er.AttributeExpr.Type.(expr.UserType); ok {
+	if ut, ok := er.Type.(expr.UserType); ok {
 		pkg = codegen.UserTypeLocation(ut).PackageName()
 	}
 	return &ErrorInitData{
@@ -1098,7 +1113,7 @@ func buildMethodData(m *expr.MethodExpr, scope *codegen.NameScope) *MethodData {
 		errorLocs = make(map[string]*codegen.Location, len(m.Errors))
 		for i, er := range m.Errors {
 			errors[i] = buildErrorInitData(er, scope)
-			errorLocs[er.Name] = codegen.UserTypeLocation(er.AttributeExpr.Type)
+			errorLocs[er.Name] = codegen.UserTypeLocation(er.Type)
 		}
 	}
 	for _, req := range m.Requirements {
@@ -1638,7 +1653,7 @@ func buildProjectedType(projected, att *expr.AttributeExpr, viewspkg string, sco
 func buildViews(rt *expr.ResultTypeExpr, viewScope *codegen.NameScope) []*ViewData {
 	views := make([]*ViewData, len(rt.Views))
 	for i, view := range rt.Views {
-		vatt := expr.AsObject(view.AttributeExpr.Type)
+		vatt := expr.AsObject(view.Type)
 		attrs := make([]string, len(*vatt))
 		for j, nat := range *vatt {
 			attrs[j] = nat.Name
@@ -2072,7 +2087,7 @@ func buildConstructorCode(src, tgt *expr.AttributeExpr, sourceVar, targetVar str
 		finit := "new" + targetCtx.Scope.Name(nat.Attribute, "", targetCtx.Pointer, targetCtx.UseDefault)
 		if view != "" {
 			v := ""
-			if vatt := rt.View(view).AttributeExpr.Find(nat.Name); vatt != nil {
+			if vatt := rt.View(view).Find(nat.Name); vatt != nil {
 				if attv, ok := vatt.Meta.Last(expr.ViewMetaKey); ok && attv != expr.DefaultView {
 					// view is explicitly set for the result type on the attribute
 					v = attv
diff --git a/codegen/service/templates/service.go.tpl b/codegen/service/templates/service.go.tpl
index 6a3334a5..396b8252 100644
--- a/codegen/service/templates/service.go.tpl
+++ b/codegen/service/templates/service.go.tpl
@@ -29,7 +29,7 @@ type Service interface {
 {{- if .Schemes }}
 // Auther defines the authorization functions to be implemented by the service.
 type Auther interface {
-	{{- range .Schemes }}
+	{{- range .Schemes.DedupeByType }}
 	{{ printf "%sAuth implements the authorization logic for the %s security scheme." .Type .Type | comment }}
 	{{ .Type }}Auth(ctx context.Context, {{ if eq .Type "Basic" }}user, pass{{ else if eq .Type "APIKey" }}key{{ else }}token{{ end }} string, schema *security.{{ .Type }}Scheme) (context.Context, error)
 	{{- end }}
diff --git a/codegen/service/templates/service_endpoint_method.go.tpl b/codegen/service/templates/service_endpoint_method.go.tpl
index ccc654e0..dca0391e 100644
--- a/codegen/service/templates/service_endpoint_method.go.tpl
+++ b/codegen/service/templates/service_endpoint_method.go.tpl
@@ -1,7 +1,7 @@
 
 
 {{ printf "New%sEndpoint returns an endpoint function that calls the method %q of service %q." .VarName .Name .ServiceName | comment }}
-func New{{ .VarName }}Endpoint(s {{ .ServiceVarName }}{{ range .Schemes }}, auth{{ .Type }}Fn security.Auth{{ .Type }}Func{{ end }}) goa.Endpoint {
+func New{{ .VarName }}Endpoint(s {{ .ServiceVarName }}{{ range .Schemes.DedupeByType }}, auth{{ .Type }}Fn security.Auth{{ .Type }}Func{{ end }}) goa.Endpoint {
 	return func(ctx context.Context, req any) (any, error) {
 {{- if or .ServerStream }}
 		ep := req.(*{{ .ServerStream.EndpointStruct }})
diff --git a/codegen/service/templates/service_endpoints_init.go.tpl b/codegen/service/templates/service_endpoints_init.go.tpl
index b1c9e6c4..38770a22 100644
--- a/codegen/service/templates/service_endpoints_init.go.tpl
+++ b/codegen/service/templates/service_endpoints_init.go.tpl
@@ -11,7 +11,7 @@ func New{{ .VarName }}(s {{ .ServiceVarName }}{{ if .HasServerInterceptors }}, s
 	return &{{ .VarName }}{
 {{- end }}
 {{- range .Methods }}
-		{{ .VarName }}: New{{ .VarName }}Endpoint(s{{ range .Schemes }}, a.{{ .Type }}Auth{{ end }}),
+		{{ .VarName }}: New{{ .VarName }}Endpoint(s{{ range .Schemes.DedupeByType }}, a.{{ .Type }}Auth{{ end }}),
 {{- end }}
 	}
 {{- if .HasServerInterceptors }}
diff --git a/dsl/http.go b/dsl/http.go
index f6aaf7e3..d1c9f025 100644
--- a/dsl/http.go
+++ b/dsl/http.go
@@ -914,7 +914,7 @@ func Body(args ...any) {
 		}
 		kind = "Request"
 	case *expr.HTTPErrorExpr:
-		ref = e.ErrorExpr.AttributeExpr
+		ref = e.AttributeExpr
 		setter = func(att *expr.AttributeExpr) {
 			if e.Response == nil {
 				e.Response = &expr.HTTPResponseExpr{}
diff --git a/dsl/sse.go b/dsl/sse.go
new file mode 100644
index 00000000..7d47684d
--- /dev/null
+++ b/dsl/sse.go
@@ -0,0 +1,300 @@
+package dsl
+
+import (
+	"goa.design/goa/v3/eval"
+	"goa.design/goa/v3/expr"
+)
+
+// ServerSentEvents specifies that a streaming endpoint should use the
+// Server-Sent Events protocol for streaming instead of WebSockets. It can be
+// used in four ways:
+//
+//  1. ServerSentEvents(): StreamingResult type is used directly as the event
+//     "data" field (serialized into JSON if not a primitive type)
+//  2. ServerSentEvents("attributeName"): The specified attribute is used as the
+//     event "data" field (serialized into JSON if not a primitive type)
+//  3. ServerSentEvents(func() { ... }): Custom mapping of attributes to event
+//     fields
+//  4. ServerSentEvents("attributeName", func() { ... }): Define attribute name
+//     used as the "data" field and custom mapping for others.
+//
+// ServerSentEvents can appear in an API HTTP expression (to specify SSE for all streaming
+// methods in the API), in a Service HTTP expression (to specify SSE for all streaming
+// methods in the service), or in a Method HTTP expression. When specified at the
+// API or service level, any method with a StreamingPayload will fall back to using WebSockets
+// as SSE only supports server-to-client streaming.
+//
+// See SSEEventData, SSEEventID, SSEEventType, SSEEventRetry for more details on
+// mapping result attributes to event fields. See SSERequestID for more details on
+// mapping payload attributes to the Last-Event-ID request header.
+//
+// Example:
+//
+//	var Notification = Type("Notification", func() {
+//	    Attribute("message", String)
+//	    Attribute("timestamp", String)
+//	    Required("message", "timestamp")
+//	})
+//
+//	var _ = Service("events", func() {
+//	    HTTP(func() {
+//	        ServerSentEvents() // All streaming methods in this service use SSE by default
+//	    })
+//
+//	    // Simple method with just data field
+//	    Method("stream", func() {
+//	        StreamingResult(Notification)
+//	        HTTP(func() {
+//	            GET("/events") // Messages are sent as {"data": {"message": <message>, "timestamp": <timestamp>}}
+//	        })
+//	    })
+//	})
+//
+//	var _ = Service("other", func() {
+//	    // Method using WebSockets
+//	    Method("stream", func() {
+//	        StreamingResult(Notification)
+//	        HTTP(func() {
+//	            GET("/websocket")
+//	        })
+//	    })
+//
+//	    // Method using SSE
+//	    Method("stream", func() {
+//	        Payload(func() {
+//	            Attribute("id", String)
+//	        })
+//	        StreamingResult(Notification)
+//	        HTTP(func() {
+//	            ServerSentEvents(func() {   // Use SSE for this method
+//	                SSERequestID("id")      // Use payload "id" field to set "Last-Event-Id" request header
+//	                SSEEventID("timestamp") // Use result "timestamp" attribute for "id" event field
+//	                SSEEventData("message") // Use result "message" attribute for "data" event field
+//	            })
+//	            GET("/sse") // Messages are sent as {"id": <timestamp>, "data": <message>}
+//	        })
+//	    })
+//	})
+func ServerSentEvents(args ...any) {
+	if len(args) > 2 {
+		eval.TooManyArgError()
+		return
+	}
+
+	var fn func()
+	var dataField string
+	if len(args) > 0 {
+		switch actual := args[0].(type) {
+		case func():
+			fn = actual
+		case string:
+			dataField = actual
+		case nil:
+			// Use the entire result as data field
+		default:
+			eval.InvalidArgError("function or string", args[0])
+			return
+		}
+		if len(args) == 2 {
+			if fn != nil {
+				eval.TooManyArgError()
+				return
+			}
+			var ok bool
+			fn, ok = args[1].(func())
+			if !ok {
+				eval.InvalidArgError("function", args[1])
+				return
+			}
+		}
+	}
+
+	sse := &expr.HTTPSSEExpr{
+		DataField: dataField,
+	}
+
+	switch actual := eval.Current().(type) {
+	case *expr.HTTPExpr:
+		actual.SSE = sse
+	case *expr.HTTPServiceExpr:
+		actual.SSE = sse
+	case *expr.HTTPEndpointExpr:
+		actual.SSE = sse
+	default:
+		eval.IncompatibleDSL()
+	}
+
+	if fn != nil {
+		eval.Execute(fn, sse)
+	}
+}
+
+// SSERequestID defines the attribute of the Payload type that provides the
+// Last-Event-ID request header value. The attribute must exist in the Payload
+// type and must be of type String.
+//
+// SSERequestID must appear in a `ServerSentEvents` expression.
+//
+// SSERequestID accepts a single argument: the name of the attribute of the
+// Payload type that provides the Last-Event-ID request header value.
+//
+// Example:
+//
+//	Method("stream", func() {
+//	    Payload(func() {
+//	        Attribute("id", String)
+//	    })
+//	    StreamingResult(Notification)
+//	    HTTP(func() {
+//	        GET("/events")
+//	        ServerSentEvents(func() {   // Use SSE for this method
+//	            SSERequestID("id")      // Use payload "id" field to set "Last-Event-Id" request header
+//	            SSEEventID("timestamp") // Use result "timestamp" attribute for "id" event field
+//	            SSEEventData("message") // Use result "message" attribute for "data" event field
+//	        })
+//	    })
+//	})
+func SSERequestID(name string) {
+	if name == "" {
+		eval.ReportError("request ID field name cannot be empty")
+		return
+	}
+	sse, ok := eval.Current().(*expr.HTTPSSEExpr)
+	if !ok {
+		eval.IncompatibleDSL()
+		return
+	}
+	sse.RequestIDField = name
+}
+
+// SSEEventData defines the attribute of the StreamingResult type that provides the
+// data field for a Server-Sent Event. The attribute must exist in the
+// StreamingResult type.
+//
+// SSEEventData must appear in a `ServerSentEvents` expression.
+//
+// SSEEventData accepts a single argument: the name of the attribute of the
+// StreamingResult type that provides the data field for a Server-Sent Event.
+//
+// Example:
+//
+//	Method("stream", func() {
+//	    StreamingResult(Payload)
+//	    HTTP(func() {
+//	        GET("/events")
+//	        ServerSentEvents(func() {
+//	            SSEEventData("message") // Use payload "message" attribute for SSE data field, other attributes are ignored
+//	        })
+//	    })
+//	})
+func SSEEventData(name string) {
+	if name == "" {
+		eval.ReportError("data field name cannot be empty")
+		return
+	}
+	sse, ok := eval.Current().(*expr.HTTPSSEExpr)
+	if !ok {
+		eval.IncompatibleDSL()
+		return
+	}
+	sse.DataField = name
+}
+
+// SSEEventID defines the attribute of the StreamingResult type that provides the
+// id field for a Server-Sent Event. The attribute must exist in the
+// StreamingResult type and must be of type String.
+//
+// SSEEventID must appear in a `ServerSentEvents` expression.
+//
+// SSEEventID accepts a single argument: the name of the attribute of the
+// StreamingResult type that provides the id field for a Server-Sent Event.
+//
+// Example:
+//
+//	Method("stream", func() {
+//	    StreamingResult(Payload)
+//	    HTTP(func() {
+//	        GET("/events")
+//	        ServerSentEvents(func() {
+//	            SSEEventID("timestamp") // Use "timestamp" attribute for SSE id field
+//	        })
+//	    })
+//	})
+func SSEEventID(name string) {
+	if name == "" {
+		eval.ReportError("id field name cannot be empty")
+		return
+	}
+	sse, ok := eval.Current().(*expr.HTTPSSEExpr)
+	if !ok {
+		eval.IncompatibleDSL()
+		return
+	}
+	sse.IDField = name
+}
+
+// SSEEventType defines the attribute of the StreamingResult type that provides the
+// event field (event type) for a Server-Sent Event. The attribute must exist in the
+// StreamingResult type and must be of type String.
+//
+// SSEEventType must appear in a `ServerSentEvents` expression.
+//
+// SSEEventType accepts a single argument: the name of the attribute of the
+// StreamingResult type that provides the event field for a Server-Sent Event.
+//
+// Example:
+//
+//	Method("stream", func() {
+//	    StreamingResult(Payload)
+//	    HTTP(func() {
+//	        GET("/events")
+//	        ServerSentEvents(func() {
+//	            SSEEventType("type") // Use payload "type" attribute for SSE event field
+//	        })
+//	    })
+//	})
+func SSEEventType(name string) {
+	if name == "" {
+		eval.ReportError("event field name cannot be empty")
+		return
+	}
+	sse, ok := eval.Current().(*expr.HTTPSSEExpr)
+	if !ok {
+		eval.IncompatibleDSL()
+		return
+	}
+	sse.EventField = name
+}
+
+// SSEEventRetry defines the attribute of the StreamingResult type that provides
+// the retry field for a Server-Sent Event. The attribute must exist in the
+// StreamingResult type and must be of type Int or UInt.
+//
+// SSEEventRetry must appear in a `ServerSentEvents` expression.
+//
+// SSEEventRetry accepts a single argument: the name of the attribute of the
+// StreamingResult type that provides the retry field for a Server-Sent Event.
+//
+// Example:
+//
+//	Method("stream", func() {
+//	    StreamingResult(Notification)
+//	    HTTP(func() {
+//	        GET("/events")
+//	        ServerSentEvents(func() {
+//	            SSEEventRetry("retry") // Use "retry" attribute for SSE retry field
+//	        })
+//	    })
+//	})
+func SSEEventRetry(name string) {
+	if name == "" {
+		eval.ReportError("retry field name cannot be empty")
+		return
+	}
+	sse, ok := eval.Current().(*expr.HTTPSSEExpr)
+	if !ok {
+		eval.IncompatibleDSL()
+		return
+	}
+	sse.RetryField = name
+}
diff --git a/expr/attribute.go b/expr/attribute.go
index 5193a291..38240488 100644
--- a/expr/attribute.go
+++ b/expr/attribute.go
@@ -665,7 +665,7 @@ func (a *AttributeExpr) debug(prefix string, seen map[*AttributeExpr]int, indent
 	if rt, ok := a.Type.(*ResultTypeExpr); ok {
 		fmt.Printf("%s%sviews\n", tabs, tab)
 		for _, v := range rt.Views {
-			nats := *AsObject(v.AttributeExpr.Type)
+			nats := *AsObject(v.Type)
 			keys := make([]string, len(nats))
 			for i, n := range nats {
 				keys[i] = n.Name
diff --git a/expr/http.go b/expr/http.go
index 557adb64..c59b0330 100644
--- a/expr/http.go
+++ b/expr/http.go
@@ -29,6 +29,9 @@ type (
 		Services []*HTTPServiceExpr
 		// Errors lists the error HTTP responses.
 		Errors []*HTTPErrorExpr
+		// SSE contains the Server-Sent Events configuration for all
+		// streaming endpoints in the API.
+		SSE *HTTPSSEExpr
 	}
 )
 
diff --git a/expr/http_body_types.go b/expr/http_body_types.go
index bd44b5b2..ea1edf25 100644
--- a/expr/http_body_types.go
+++ b/expr/http_body_types.go
@@ -195,12 +195,12 @@ func httpRequestBody(a *HTTPEndpointExpr) *AttributeExpr {
 	if t, ok := payload.Type.(UserType); ok {
 		// Remember openapi typename for example to generate friendly OpenAPI specs.
 		if m, ok := t.Attribute().Meta["openapi:typename"]; ok {
-			ut.AttributeExpr.AddMeta("openapi:typename", m...)
+			ut.AddMeta("openapi:typename", m...)
 		}
 
 		// Remember additionalProperties.
 		if m, ok := t.Attribute().Meta["openapi:additionalProperties"]; ok {
-			ut.AttributeExpr.AddMeta("openapi:additionalProperties", m...)
+			ut.AddMeta("openapi:additionalProperties", m...)
 		}
 	}
 
@@ -262,7 +262,7 @@ func httpResponseBody(a *HTTPEndpointExpr, resp *HTTPResponseExpr) *AttributeExp
 // parameters.
 func httpErrorResponseBody(e *HTTPEndpointExpr, v *HTTPErrorExpr) *AttributeExpr {
 	name := e.Name() + "_" + v.ErrorExpr.Name
-	return buildHTTPResponseBody(name, v.ErrorExpr.AttributeExpr, v.Response, e.Service)
+	return buildHTTPResponseBody(name, v.AttributeExpr, v.Response, e.Service)
 }
 
 func buildHTTPResponseBody(name string, attr *AttributeExpr, resp *HTTPResponseExpr, svc *HTTPServiceExpr) *AttributeExpr {
@@ -342,14 +342,14 @@ func buildHTTPResponseBody(name string, attr *AttributeExpr, resp *HTTPResponseE
 	if t, ok := attr.Type.(UserType); ok {
 		// Remember original type name and openapi typename for example
 		// to generate friendly OpenAPI specs.
-		userType.AttributeExpr.AddMeta("name:original", t.Name())
+		userType.AddMeta("name:original", t.Name())
 		if m, ok := t.Attribute().Meta["openapi:typename"]; ok {
-			userType.AttributeExpr.AddMeta("openapi:typename", m...)
+			userType.AddMeta("openapi:typename", m...)
 		}
 
 		// Remember additionalProperties.
 		if m, ok := t.Attribute().Meta["openapi:additionalProperties"]; ok {
-			userType.AttributeExpr.AddMeta("openapi:additionalProperties", m...)
+			userType.AddMeta("openapi:additionalProperties", m...)
 		}
 	}
 
diff --git a/expr/http_endpoint.go b/expr/http_endpoint.go
index bc6c92a6..4ce5fdba 100644
--- a/expr/http_endpoint.go
+++ b/expr/http_endpoint.go
@@ -1,6 +1,7 @@
 package expr
 
 import (
+	"errors"
 	"fmt"
 	"path"
 	"strings"
@@ -66,6 +67,10 @@ type (
 		MultipartRequest bool
 		// Redirect defines a redirect for the endpoint.
 		Redirect *HTTPRedirectExpr
+		// SSE defines the Server-Sent Events configuration for this endpoint if it's
+		// a streaming endpoint. If nil, the endpoint uses WebSockets by default or
+		// inherits the service-level SSE configuration if defined.
+		SSE *HTTPSSEExpr
 		// Meta is a set of key/value pairs with semantic that is
 		// specific to each generator, see dsl.Meta.
 		Meta MetaExpr
@@ -274,6 +279,15 @@ func (e *HTTPEndpointExpr) Prepare() {
 		e.Responses = []*HTTPResponseExpr{{StatusCode: status}}
 	}
 
+	// Inherit SSE configuration from service or API level for streaming endpoints
+	if e.MethodExpr.Stream == ServerStreamKind && e.SSE == nil {
+		if e.Service.SSE != nil {
+			e.SSE = e.Service.SSE
+		} else if Root.API.HTTP.SSE != nil {
+			e.SSE = Root.API.HTTP.SSE
+		}
+	}
+
 	// Error -> ResponseError
 	methodErrors := map[string]struct{}{}
 	for _, v := range e.HTTPErrors {
@@ -377,6 +391,29 @@ func (e *HTTPEndpointExpr) Validate() error {
 		}
 	}
 
+	// Validate streaming endpoints for SSE compatibility
+	if e.MethodExpr.Stream == ServerStreamKind {
+		// Prepare already handles inheriting SSE from service or API level
+		if e.SSE != nil {
+			if err := e.SSE.Validate(e); err != nil {
+				var valErr *eval.ValidationErrors
+				if errors.As(err, &valErr) {
+					verr.Merge(valErr)
+				}
+			}
+		}
+	} else if e.SSE != nil {
+		// Error if SSE is defined but endpoint is not server streaming
+		switch e.MethodExpr.Stream {
+		case BidirectionalStreamKind:
+			verr.Add(e, "Server-Sent Events cannot be used with bidirectional streaming endpoints")
+		case ClientStreamKind:
+			verr.Add(e, "Server-Sent Events cannot be used with client-to-server streaming endpoints")
+		default:
+			verr.Add(e, "Server-Sent Events can only be used with endpoints that have a streaming result")
+		}
+	}
+
 	// Redirect is not compatible with Response.
 	if e.Redirect != nil {
 		found := false
@@ -643,10 +680,13 @@ func (e *HTTPEndpointExpr) Validate() error {
 	if e.SkipRequestBodyEncodeDecode && body.Type != Empty {
 		verr.Add(e, "HTTP endpoint request body must be empty when using SkipRequestBodyEncodeDecode but not all method payload attributes are mapped to headers and params. Make sure to define Headers and Params as needed.")
 	}
+	// For streaming endpoints, check if request body is allowed
 	if e.MethodExpr.IsStreaming() && body.Type != Empty {
-		// Refer Websocket protocol - https://tools.ietf.org/html/rfc6455
-		// Protocol does not allow HTTP request body to be passed.
-		verr.Add(e, "HTTP endpoint request body must be empty when the endpoint uses streaming. Payload attributes must be mapped to headers and/or params.")
+		// SSE endpoints can have request bodies, but WebSocket endpoints cannot
+		// Refer WebSocket protocol - https://tools.ietf.org/html/rfc6455
+		if e.SSE == nil { // Only apply this validation to non-SSE streaming endpoints
+			verr.Add(e, "HTTP endpoint request body must be empty when the endpoint uses streaming. Payload attributes must be mapped to headers and/or params.")
+		}
 	}
 
 	return verr
diff --git a/expr/http_service.go b/expr/http_service.go
index 1944ed92..1e1a1799 100644
--- a/expr/http_service.go
+++ b/expr/http_service.go
@@ -40,6 +40,9 @@ type (
 		HTTPErrors []*HTTPErrorExpr
 		// FileServers is the list of static asset serving endpoints
 		FileServers []*HTTPFileServerExpr
+		// SSE defines the Server-Sent Events configuration for all streaming endpoints
+		// in this service. If nil, streaming endpoints use WebSockets by default.
+		SSE *HTTPSSEExpr
 		// Meta is a set of key/value pairs with semantic that is
 		// specific to each generator.
 		Meta MetaExpr
diff --git a/expr/http_sse.go b/expr/http_sse.go
new file mode 100644
index 00000000..3b7ddfcd
--- /dev/null
+++ b/expr/http_sse.go
@@ -0,0 +1,103 @@
+package expr
+
+import (
+	"fmt"
+	"strings"
+
+	"slices"
+
+	"goa.design/goa/v3/eval"
+)
+
+type (
+	// HTTPSSEExpr describes a Server-Sent Events configuration for a HTTP endpoint.
+	// It defines how a streaming endpoint should use the Server-Sent Events protocol
+	// instead of WebSockets.
+	HTTPSSEExpr struct {
+		// RequestIDField is the name of the attribute in the Payload type
+		// that provides the Last-Event-ID request header value.
+		// If empty, no Last-Event-ID request header is included in the request.
+		RequestIDField string
+		// DataField is the name of the attribute in the StreamingResult type
+		// that provides the data field for a Server-Sent Event.
+		// If empty, the entire StreamingResult is used as the data field.
+		DataField string
+		// IDField is the name of the attribute in the StreamingResult type
+		// that provides the id field for a Server-Sent Event.
+		// If empty, no id field is included in the event.
+		IDField string
+		// EventField is the name of the attribute in the StreamingResult type
+		// that provides the event field (event type) for a Server-Sent Event.
+		// If empty, no event field is included in the event.
+		EventField string
+		// RetryField is the name of the attribute in the StreamingResult type
+		// that provides the retry field for a Server-Sent Event.
+		// If empty, no retry field is included in the event.
+		RetryField string
+	}
+)
+
+// EvalName returns the generic expression name used in error messages.
+func (e *HTTPSSEExpr) EvalName() string {
+	return "Server-Sent Events"
+}
+
+// Validate validates the Server-Sent Events expression against a specific result type.
+func (e *HTTPSSEExpr) Validate(endpoint *HTTPEndpointExpr) error {
+	if endpoint == nil || endpoint.MethodExpr == nil || endpoint.MethodExpr.Result == nil {
+		return nil
+	}
+
+	verr := new(eval.ValidationErrors)
+	if err := validateSSEField(endpoint.MethodExpr.Payload, e.RequestIDField, "request ID", []DataType{String}); err != nil {
+		verr.Add(endpoint, err.Error())
+	}
+	if err := validateSSEField(endpoint.MethodExpr.Result, e.DataField, "event data", nil); err != nil {
+		verr.Add(endpoint, err.Error())
+	}
+	if err := validateSSEField(endpoint.MethodExpr.Result, e.IDField, "event id", []DataType{String}); err != nil {
+		verr.Add(endpoint, err.Error())
+	}
+	if err := validateSSEField(endpoint.MethodExpr.Result, e.EventField, "event type", []DataType{String}); err != nil {
+		verr.Add(endpoint, err.Error())
+	}
+	if err := validateSSEField(endpoint.MethodExpr.Result, e.RetryField, "event retry", []DataType{Int, Int32, Int64, UInt, UInt32, UInt64}); err != nil {
+		verr.Add(endpoint, err.Error())
+	}
+
+	if len(verr.Errors) == 0 {
+		return nil
+	}
+	return verr
+}
+
+// validateSSEField validates that the given field exists in the result type and has the expected type.
+func validateSSEField(rt *AttributeExpr, field, desc string, expectedTypes []DataType) error {
+	if field == "" {
+		return nil
+	}
+
+	if rt == nil {
+		return fmt.Errorf("cannot use %q for SSE %s field: result type is nil", field, desc)
+	}
+
+	obj := AsObject(rt.Type)
+	if obj == nil {
+		return fmt.Errorf("cannot use %q for SSE %s field: result type is not an object", field, desc)
+	}
+
+	att := obj.Attribute(field)
+	if att == nil {
+		return fmt.Errorf("cannot use %q for SSE %s field: attribute not found in result type", field, desc)
+	}
+
+	if len(expectedTypes) > 0 && !slices.Contains(expectedTypes, att.Type) {
+		typeNames := make([]string, len(expectedTypes))
+		for i, t := range expectedTypes {
+			typeNames[i] = t.Name()
+		}
+		return fmt.Errorf("cannot use %q for SSE %s field: attribute type must be one of %s", field, desc, strings.Join(typeNames, ", "))
+	}
+
+	return nil
+}
diff --git a/expr/mapped_attribute.go b/expr/mapped_attribute.go
index a9bcdae2..6543127d 100644
--- a/expr/mapped_attribute.go
+++ b/expr/mapped_attribute.go
@@ -77,7 +77,7 @@ func (ma *MappedAttributeExpr) Remap() {
 	}
 
 	// Conserve examples defined on user type
-	ma.AttributeExpr.UserExamples = ma.ExtractUserExamples()
+	ma.UserExamples = ma.ExtractUserExamples()
 
 	ma.Type = n
 }
diff --git a/expr/method.go b/expr/method.go
index 0183545e..b7a8f450 100644
--- a/expr/method.go
+++ b/expr/method.go
@@ -227,7 +227,7 @@ func (m *MethodExpr) validateErrors() *eval.ValidationErrors {
 					return nil
 				})
 				if !found {
-					verr.Add(e, "type %q is used to define multiple errors and must identify the attribute containing the error name with ErrorName", e.AttributeExpr.Type.Name())
+					verr.Add(e, "type %q is used to define multiple errors and must identify the attribute containing the error name with ErrorName", e.Type.Name())
 					break
 				}
 			}
diff --git a/expr/result_type.go b/expr/result_type.go
index 8622ab9c..8a098a77 100644
--- a/expr/result_type.go
+++ b/expr/result_type.go
@@ -170,7 +170,7 @@ func (rt *ResultTypeExpr) ViewHasAttribute(view, attr string) bool {
 	if v == nil {
 		return false
 	}
-	return v.AttributeExpr.Find(attr) != nil
+	return v.Find(attr) != nil
 }
 
 // Finalize builds the default view if not explicitly defined and finalizes
@@ -195,7 +195,7 @@ func (rt *ResultTypeExpr) Finalize() {
 // useExplicitView projects the result type using the view explicitly set on the
 // attribute if any.
 func (rt *ResultTypeExpr) useExplicitView() {
-	if view, ok := rt.AttributeExpr.Meta.Last(ViewMetaKey); ok {
+	if view, ok := rt.Meta.Last(ViewMetaKey); ok {
 		p, err := Project(rt, view)
 		if err != nil {
 			panic(err) // bug - presence of view meta should have been validated before
@@ -300,8 +300,8 @@ func projectSingle(rt *ResultTypeExpr, view string, seen map[string]*AttributeEx
 	}
 	ut.TypeName = typeName
 	ut.UID = id
-	ut.AttributeExpr.Type = Dup(v.Type)
-	ut.AttributeExpr.UserExamples = v.UserExamples
+	ut.Type = Dup(v.Type)
+	ut.UserExamples = v.UserExamples
 	projected := &ResultTypeExpr{
 		Identifier:   id,
 		UserTypeExpr: ut,
diff --git a/expr/service.go b/expr/service.go
index ab906c46..43887d95 100644
--- a/expr/service.go
+++ b/expr/service.go
@@ -109,14 +109,14 @@ func (e *ErrorExpr) Validate() error {
 	walkAttribute(e.AttributeExpr, func(name string, att *AttributeExpr) error { // nolint: errcheck
 		if _, ok := att.Meta["struct:error:name"]; ok {
 			if errField != "" {
-				verr.Add(e, "duplicate error names in type %q", e.AttributeExpr.Type.Name())
+				verr.Add(e, "duplicate error names in type %q", e.Type.Name())
 			}
 			errField = name
 			if att.Type != String {
-				verr.Add(e, "error name %q must be a string in type %q", name, e.AttributeExpr.Type.Name())
+				verr.Add(e, "error name %q must be a string in type %q", name, e.Type.Name())
 			}
-			if !e.AttributeExpr.IsRequired(name) {
-				verr.Add(e, "error name %q must be required in type %q", name, e.AttributeExpr.Type.Name())
+			if !e.IsRequired(name) {
+				verr.Add(e, "error name %q must be required in type %q", name, e.Type.Name())
 			}
 		}
 		return nil
diff --git a/expr/user_type.go b/expr/user_type.go
index 33c37785..baacbb42 100644
--- a/expr/user_type.go
+++ b/expr/user_type.go
@@ -33,7 +33,7 @@ func (u *UserTypeExpr) Name() string {
 	if u.AttributeExpr == nil {
 		return u.TypeName
 	}
-	if n, ok := u.AttributeExpr.Meta["struct:type:name"]; ok {
+	if n, ok := u.Meta["struct:type:name"]; ok {
 		return n[0]
 	}
 	return u.TypeName
@@ -42,8 +42,8 @@ func (u *UserTypeExpr) Name() string {
 // Rename changes the type name to the given value.
 func (u *UserTypeExpr) Rename(n string) {
 	// Remember original name for example to generate friendly docs.
-	u.AttributeExpr.AddMeta("name:original", u.TypeName)
-	delete(u.AttributeExpr.Meta, "struct:type:name")
+	u.AddMeta("name:original", u.TypeName)
+	delete(u.Meta, "struct:type:name")
 	u.TypeName = n
 }
 
diff --git a/go.mod b/go.mod
index d8f16546..832b2600 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,7 @@ toolchain go1.23.6
 
 require (
 	github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
-	github.com/getkin/kin-openapi v0.131.0
+	github.com/getkin/kin-openapi v0.132.0
 	github.com/go-chi/chi/v5 v5.2.1
 	github.com/gohugoio/hashstructure v0.5.0
 	github.com/google/uuid v1.6.0
@@ -14,9 +14,9 @@ require (
 	github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
 	github.com/pkg/errors v0.9.1
 	github.com/stretchr/testify v1.10.0
-	golang.org/x/text v0.23.0
-	golang.org/x/tools v0.31.0
-	google.golang.org/grpc v1.71.0
+	golang.org/x/text v0.24.0
+	golang.org/x/tools v0.32.0
+	google.golang.org/grpc v1.72.0
 	google.golang.org/protobuf v1.36.6
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -34,8 +34,8 @@ require (
 	github.com/perimeterx/marshmallow v1.1.5 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	golang.org/x/mod v0.24.0 // indirect
-	golang.org/x/net v0.37.0 // indirect
-	golang.org/x/sync v0.12.0 // indirect
-	golang.org/x/sys v0.31.0 // indirect
-	google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 // indirect
+	golang.org/x/net v0.39.0 // indirect
+	golang.org/x/sync v0.13.0 // indirect
+	golang.org/x/sys v0.32.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect
 )
diff --git a/go.sum b/go.sum
index 076c1301..7dce2816 100644
--- a/go.sum
+++ b/go.sum
@@ -4,8 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI=
 github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598/go.mod h1:0FpDmbrt36utu8jEmeU05dPC9AB5tsLYVVi+ZHfyuwI=
-github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE=
-github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
+github.com/getkin/kin-openapi v0.132.0 h1:3ISeLMsQzcb5v26yeJrBcdTCEQTag36ZjaGk7MIRUwk=
+github.com/getkin/kin-openapi v0.132.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
 github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
 github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
@@ -72,20 +72,20 @@ go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC
 go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
 golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
 golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
-golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
-golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
-golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
-golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
-golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
-golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
-golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
-golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
-golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2 h1:DMTIbak9GhdaSxEjvVzAeNZvyc03I61duqNbnm3SU0M=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20250219182151-9fdb1cabc7b2/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
-google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
-google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
+golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
+golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
+golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
+golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
+golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
+golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
+golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU=
+golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
+google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM=
 google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
 google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
diff --git a/grpc/codegen/protobuf_transform.go b/grpc/codegen/protobuf_transform.go
index c98e657c..fd16ef42 100644
--- a/grpc/codegen/protobuf_transform.go
+++ b/grpc/codegen/protobuf_transform.go
@@ -85,12 +85,12 @@ func protoBufTransform(source, target *expr.AttributeExpr, sourceVar, targetVar
 	if proto {
 		target = expr.DupAtt(target)
 		removeMeta(target)
-		ta.TransformAttrs.Prefix = "svc"
+		ta.Prefix = "svc"
 		ta.proto = true
 	} else {
 		source = expr.DupAtt(source)
 		removeMeta(source)
-		ta.TransformAttrs.Prefix = "protobuf"
+		ta.Prefix = "protobuf"
 		ta.proto = false
 	}
 
@@ -268,7 +268,7 @@ func transformObject(source, target *expr.AttributeExpr, sourceVar, targetVar st
 		assign = ":="
 	}
 	tname := ta.TargetCtx.Scope.Name(target, ta.TargetCtx.Pkg(target), ta.TargetCtx.Pointer, ta.TargetCtx.UseDefault)
-	buffer.WriteString(fmt.Sprintf("%s %s %s%s{%s}\n", targetVar, assign, deref, tname, initCode))
+	fmt.Fprintf(buffer, "%s %s %s%s{%s}\n", targetVar, assign, deref, tname, initCode)
 	buffer.WriteString(postInitCode)
 
 	// iterate through attributes to initialize rest of the struct fields and
diff --git a/grpc/codegen/service_data.go b/grpc/codegen/service_data.go
index 442d8d0f..68678b72 100644
--- a/grpc/codegen/service_data.go
+++ b/grpc/codegen/service_data.go
@@ -462,7 +462,7 @@ func (ServicesData) analyze(gs *expr.GRPCServiceExpr) *ServiceData {
 		}
 		e.Response.Message = makeProtoBufMessage(e.Response.Message, protoBufify(e.Name()+"_response", true, true), sd)
 		for _, er := range e.GRPCErrors {
-			if er.ErrorExpr.Type == expr.ErrorResult || !expr.IsObject(er.ErrorExpr.Type) {
+			if er.Type == expr.ErrorResult || !expr.IsObject(er.Type) {
 				continue
 			}
 			er.Response.Message = makeProtoBufMessage(er.Response.Message, protoBufify(e.Name()+"_"+er.Name+"_error", true, true), sd)
@@ -519,7 +519,7 @@ func (ServicesData) analyze(gs *expr.GRPCServiceExpr) *ServiceData {
 		}
 		errors := buildErrorsData(e, sd)
 		for _, er := range e.GRPCErrors {
-			if er.ErrorExpr.Type == expr.ErrorResult || !expr.IsObject(er.ErrorExpr.Type) {
+			if er.Type == expr.ErrorResult || !expr.IsObject(er.Type) {
 				continue
 			}
 			collect(er.Response.Message)
@@ -1028,7 +1028,7 @@ func buildErrorsData(e *expr.GRPCEndpointExpr, sd *ServiceData) []*ErrorData {
 		errorLoc := svc.Method(e.MethodExpr.Name).ErrorLocs[v.Name]
 		errors = append(errors, &ErrorData{
 			Name:     v.Name,
-			Ref:      svc.Scope.GoFullTypeRef(v.ErrorExpr.AttributeExpr, pkgWithDefault(errorLoc, svc.PkgName)),
+			Ref:      svc.Scope.GoFullTypeRef(v.AttributeExpr, pkgWithDefault(errorLoc, svc.PkgName)),
 			Response: responseData,
 		})
 	}
@@ -1038,19 +1038,19 @@ func buildErrorsData(e *expr.GRPCEndpointExpr, sd *ServiceData) []*ErrorData {
 func buildErrorConvertData(ge *expr.GRPCErrorExpr, e *expr.GRPCEndpointExpr, sd *ServiceData, svr bool) *ConvertData {
 	// No need to build transformation functions for default error or non-object
 	// types.
-	if ge.ErrorExpr.Type == expr.ErrorResult || !expr.IsObject(ge.ErrorExpr.Type) {
+	if ge.Type == expr.ErrorResult || !expr.IsObject(ge.Type) {
 		return nil
 	}
 	svc := sd.Service
 	svcCtx := serviceTypeContext(svc.PkgName, svc.Scope)
 	if svr {
 		// server side
-		data := buildInitData(ge.ErrorExpr.AttributeExpr, ge.Response.Message, "er", "message", svcCtx, true, svr, false, sd)
+		data := buildInitData(ge.AttributeExpr, ge.Response.Message, "er", "message", svcCtx, true, svr, false, sd)
 		data.Name = fmt.Sprintf("New%s%sError", codegen.Goify(e.Name(), true), codegen.Goify(ge.Name, true))
 		data.Description = fmt.Sprintf("%s builds the gRPC error response type from the error of the %q endpoint of the %q service.", data.Name, e.Name(), svc.Name)
 		return &ConvertData{
-			SrcName: svcCtx.Scope.Name(ge.ErrorExpr.AttributeExpr, svcCtx.Pkg(ge.ErrorExpr.AttributeExpr), svcCtx.Pointer, svcCtx.UseDefault),
-			SrcRef:  svcCtx.Scope.Ref(ge.ErrorExpr.AttributeExpr, svcCtx.Pkg(ge.ErrorExpr.AttributeExpr)),
+			SrcName: svcCtx.Scope.Name(ge.AttributeExpr, svcCtx.Pkg(ge.AttributeExpr), svcCtx.Pointer, svcCtx.UseDefault),
+			SrcRef:  svcCtx.Scope.Ref(ge.AttributeExpr, svcCtx.Pkg(ge.AttributeExpr)),
 			TgtName: protoBufGoFullTypeName(ge.Response.Message, sd.PkgName, sd.Scope),
 			TgtRef:  protoBufGoFullTypeRef(ge.Response.Message, sd.PkgName, sd.Scope),
 			Init:    data,
@@ -1058,14 +1058,14 @@ func buildErrorConvertData(ge *expr.GRPCErrorExpr, e *expr.GRPCEndpointExpr, sd
 	}
 
 	// client side
-	data := buildInitData(ge.Response.Message, ge.ErrorExpr.AttributeExpr, "message", "er", svcCtx, false, svr, false, sd)
+	data := buildInitData(ge.Response.Message, ge.AttributeExpr, "message", "er", svcCtx, false, svr, false, sd)
 	data.Name = fmt.Sprintf("New%s%sError", codegen.Goify(e.Name(), true), codegen.Goify(ge.Name, true))
 	data.Description = fmt.Sprintf("%s builds the error type of the %q endpoint of the %q service from the gRPC error response type.", data.Name, e.Name(), svc.Name)
 	return &ConvertData{
 		SrcName:    protoBufGoFullTypeName(ge.Response.Message, sd.PkgName, sd.Scope),
 		SrcRef:     protoBufGoFullTypeRef(ge.Response.Message, sd.PkgName, sd.Scope),
-		TgtName:    svcCtx.Scope.Name(ge.ErrorExpr.AttributeExpr, svcCtx.Pkg(ge.ErrorExpr.AttributeExpr), svcCtx.Pointer, svcCtx.UseDefault),
-		TgtRef:     svcCtx.Scope.Ref(ge.ErrorExpr.AttributeExpr, svcCtx.Pkg(ge.ErrorExpr.AttributeExpr)),
+		TgtName:    svcCtx.Scope.Name(ge.AttributeExpr, svcCtx.Pkg(ge.AttributeExpr), svcCtx.Pointer, svcCtx.UseDefault),
+		TgtRef:     svcCtx.Scope.Ref(ge.AttributeExpr, svcCtx.Pkg(ge.AttributeExpr)),
 		Init:       data,
 		Validation: addValidation(ge.Response.Message, "errmsg", sd, false),
 	}
diff --git a/http/client.go b/http/client.go
index b25763f8..3e7ae875 100644
--- a/http/client.go
+++ b/http/client.go
@@ -101,7 +101,7 @@ func (dd *debugDoer) Fprint(w io.Writer) {
 		return
 	}
 	buf := &bytes.Buffer{}
-	buf.WriteString(fmt.Sprintf("> %s %s", dd.Request.Method, dd.Request.URL.String())) // nolint: errcheck
+	fmt.Fprintf(buf, "> %s %s", dd.Request.Method, dd.Request.URL.String())
 
 	keys := make([]string, len(dd.Request.Header))
 	i := 0
@@ -111,7 +111,7 @@ func (dd *debugDoer) Fprint(w io.Writer) {
 	}
 	sort.Strings(keys)
 	for _, k := range keys {
-		buf.WriteString(fmt.Sprintf("\n> %s: %s", k, strings.Join(dd.Request.Header[k], ", "))) // nolint: errcheck
+		fmt.Fprintf(buf, "\n> %s: %s", k, strings.Join(dd.Request.Header[k], ", ")) // nolint: errcheck
 	}
 
 	b, _ := io.ReadAll(dd.Request.Body)
@@ -125,7 +125,7 @@ func (dd *debugDoer) Fprint(w io.Writer) {
 		w.Write(buf.Bytes()) // nolint: errcheck
 		return
 	}
-	buf.WriteString(fmt.Sprintf("\n< %s", dd.Response.Status))
+	fmt.Fprintf(buf, "\n< %s", dd.Response.Status)
 
 	keys = make([]string, len(dd.Response.Header))
 	i = 0
@@ -135,7 +135,7 @@ func (dd *debugDoer) Fprint(w io.Writer) {
 	}
 	sort.Strings(keys)
 	for _, k := range keys {
-		buf.WriteString(fmt.Sprintf("\n< %s: %s", k, strings.Join(dd.Response.Header[k], ", "))) // nolint: errcheck
+		fmt.Fprintf(buf, "\n< %s: %s", k, strings.Join(dd.Response.Header[k], ", ")) // nolint: errcheck
 	}
 
 	rb, _ := io.ReadAll(dd.Response.Body) // this is reading from a memory buffer so safe to ignore errors
diff --git a/http/codegen/client.go b/http/codegen/client.go
index 5d70b2ed..14e68654 100644
--- a/http/codegen/client.go
+++ b/http/codegen/client.go
@@ -17,6 +17,9 @@ func ClientFiles(genpkg string, root *expr.RootExpr) []*codegen.File {
 		if f := websocketClientFile(genpkg, svc); f != nil {
 			files = append(files, f)
 		}
+		if f := sseClientFile(genpkg, svc); f != nil {
+			files = append(files, f)
+		}
 	}
 	for _, svc := range root.API.HTTP.Services {
 		if f := clientEncodeDecodeFile(genpkg, svc); f != nil {
@@ -50,10 +53,13 @@ func clientFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.File {
 		}),
 	}
 	sections = append(sections, &codegen.SectionTemplate{
-		Name:    "client-struct",
-		Source:  readTemplate("client_struct"),
-		Data:    data,
-		FuncMap: map[string]any{"hasWebSocket": hasWebSocket},
+		Name:   "client-struct",
+		Source: readTemplate("client_struct"),
+		Data:   data,
+		FuncMap: map[string]any{
+			"hasWebSocket": hasWebSocket,
+			"hasSSE":       hasSSE,
+		},
 	})
 
 	for _, e := range data.Endpoints {
@@ -67,10 +73,13 @@ func clientFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.File {
 	}
 
 	sections = append(sections, &codegen.SectionTemplate{
-		Name:    "http-client-init",
-		Source:  readTemplate("client_init"),
-		Data:    data,
-		FuncMap: map[string]any{"hasWebSocket": hasWebSocket},
+		Name:   "http-client-init",
+		Source: readTemplate("client_init"),
+		Data:   data,
+		FuncMap: map[string]any{
+			"hasWebSocket": hasWebSocket,
+			"hasSSE":       hasSSE,
+		},
 	})
 
 	for _, e := range data.Endpoints {
@@ -80,6 +89,7 @@ func clientFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.File {
 			Data:   e,
 			FuncMap: map[string]any{
 				"isWebSocketEndpoint": isWebSocketEndpoint,
+				"isSSEEndpoint":       isSSEEndpoint,
 				"responseStructPkg":   responseStructPkg,
 			},
 		})
diff --git a/http/codegen/client_cli.go b/http/codegen/client_cli.go
index da4fa238..ad85e35d 100644
--- a/http/codegen/client_cli.go
+++ b/http/codegen/client_cli.go
@@ -14,8 +14,8 @@ type commandData struct {
 	*cli.CommandData
 	// Subcommands is the list of endpoint commands.
 	Subcommands []*subcommandData
-	// NeedStream if true initializes the websocket dialer.
-	NeedStream bool
+	// NeedDialer if true initializes the websocket dialer.
+	NeedDialer bool
 }
 
 // commandData wraps the common SubcommandData and adds HTTP-specific fields.
@@ -50,7 +50,7 @@ func ClientCLIFiles(genpkg string, root *expr.RootExpr) []*codegen.File {
 		if len(sd.Endpoints) > 0 {
 			command := &commandData{
 				CommandData: cli.BuildCommandData(sd.Service),
-				NeedStream:  hasWebSocket(sd),
+				NeedDialer:  hasWebSocket(sd),
 			}
 
 			for _, e := range sd.Endpoints {
@@ -292,7 +292,7 @@ func streamFlag(svcn, en string) *cli.FlagData {
 // uses stream for sending payload/result.
 func streamingCmdExists(data []*commandData) bool {
 	for _, c := range data {
-		if c.NeedStream {
+		if c.NeedDialer {
 			return true
 		}
 	}
diff --git a/http/codegen/example_cli.go b/http/codegen/example_cli.go
index c6d57c83..937f6067 100644
--- a/http/codegen/example_cli.go
+++ b/http/codegen/example_cli.go
@@ -85,7 +85,7 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg
 				"Services": svcData,
 			},
 			FuncMap: map[string]any{
-				"needStream": needStream,
+				"needDialer": needDialer,
 			},
 		},
 		{
@@ -96,7 +96,7 @@ func exampleCLI(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *codeg
 				"APIPkg":   apiPkg,
 			},
 			FuncMap: map[string]any{
-				"needStream":   needStream,
+				"needDialer":   needDialer,
 				"hasWebSocket": hasWebSocket,
 			},
 		},
diff --git a/http/codegen/example_server.go b/http/codegen/example_server.go
index ffa9835a..829781bd 100644
--- a/http/codegen/example_server.go
+++ b/http/codegen/example_server.go
@@ -105,7 +105,7 @@ func exampleServer(genpkg string, root *expr.RootExpr, svr *expr.ServerExpr) *co
 				"Services": svcdata,
 				"APIPkg":   apiPkg,
 			},
-			FuncMap: map[string]any{"needStream": needStream, "hasWebSocket": hasWebSocket},
+			FuncMap: map[string]any{"needDialer": needDialer, "hasWebSocket": hasWebSocket},
 		},
 		{
 			Name:   "server-http-middleware",
diff --git a/http/codegen/openapi/v3/types.go b/http/codegen/openapi/v3/types.go
index 5509dff4..be232d49 100644
--- a/http/codegen/openapi/v3/types.go
+++ b/http/codegen/openapi/v3/types.go
@@ -439,7 +439,7 @@ func hashAttribute(att *expr.AttributeExpr, h hash.Hash64, seen map[string]*uint
 		// the computation of the hash.
 		rt := t.(*expr.ResultTypeExpr)
 		*res = hashString(rt.Identifier, h)
-		if view, ok := rt.AttributeExpr.Meta.Last(expr.ViewMetaKey); ok {
+		if view, ok := rt.Meta.Last(expr.ViewMetaKey); ok {
 			*res = orderedHash(*res, hashString(view, h), h)
 		}
 
diff --git a/http/codegen/server.go b/http/codegen/server.go
index c514baa2..93d8428d 100644
--- a/http/codegen/server.go
+++ b/http/codegen/server.go
@@ -20,6 +20,9 @@ func ServerFiles(genpkg string, root *expr.RootExpr) []*codegen.File {
 		if f := websocketServerFile(genpkg, svc); f != nil {
 			files = append(files, f)
 		}
+		if f := sseServerFile(genpkg, svc); f != nil {
+			files = append(files, f)
+		}
 	}
 	for _, svc := range root.API.HTTP.Services {
 		if f := serverEncodeDecodeFile(genpkg, svc); f != nil {
@@ -39,6 +42,7 @@ func serverFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.File {
 		"join":                strings.Join,
 		"hasWebSocket":        hasWebSocket,
 		"isWebSocketEndpoint": isWebSocketEndpoint,
+		"isSSEEndpoint":       isSSEEndpoint,
 		"viewedServerBody":    viewedServerBody,
 		"mustDecodeRequest":   mustDecodeRequest,
 		"addLeadingSlash":     addLeadingSlash,
@@ -266,10 +270,10 @@ func viewedServerBody(sbd []*TypeData, view string) *TypeData {
 }
 
 func addLeadingSlash(s string) string {
-	if strings.HasPrefix(s, "/") {
-		return s
+	if s == "" || s[0] != '/' {
+		return "/" + s
 	}
-	return "/" + s
+	return s
 }
 
 func mapQueryDecodeData(dt expr.DataType, varName string, inc int) map[string]any {
diff --git a/http/codegen/service_data.go b/http/codegen/service_data.go
index 31950e45..2d8bf0f0 100644
--- a/http/codegen/service_data.go
+++ b/http/codegen/service_data.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"fmt"
 	"net/http"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -36,6 +37,7 @@ var (
 					_, ok := dt.(expr.UserType)
 					return ok
 				},
+				"isWebSocketEndpoint": isWebSocketEndpoint,
 			}).
 			Parse(readTemplate("request_init")),
 	)
@@ -152,6 +154,9 @@ type (
 		// ServerWebSocket holds the data to render the server struct which
 		// implements the server stream interface.
 		ServerWebSocket *WebSocketData
+		// SSE holds the data to render the server struct which implements the
+		// server stream interface for SSE.
+		SSE *SSEData
 		// Redirect defines a redirect for the endpoint.
 		Redirect *RedirectData
 
@@ -794,7 +799,7 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 			"Args":         args,
 			"PathInit":     routes[0].PathInit,
 			"Verb":         routes[0].Verb,
-			"IsStreaming":  httpEndpoint.MethodExpr.IsStreaming(),
+			"IsWebSocket":  httpEndpoint.MethodExpr.IsStreaming() && httpEndpoint.SSE == nil,
 		}
 		if httpEndpoint.SkipRequestBodyEncodeDecode {
 			data["RequestStruct"] = pkg + "." + method.RequestStruct
@@ -838,6 +843,7 @@ func (ServicesData) analyze(httpSvc *expr.HTTPServiceExpr) *ServiceData {
 		}
 		if httpEndpoint.MethodExpr.IsStreaming() {
 			initWebSocketData(ed, httpEndpoint, sd)
+			initSSEData(ed, httpEndpoint, sd)
 		}
 
 		if httpEndpoint.MultipartRequest {
@@ -1751,7 +1757,7 @@ func buildErrorsData(e *expr.HTTPEndpointExpr, sd *ServiceData) []*ErrorGroupDat
 		pkg := pkgWithDefault(ep.ErrorLocs[v.Name], svc.PkgName)
 		errctx := serviceContext(pkg, sd.Service.Scope)
 
-		if needInit(v.ErrorExpr.Type) {
+		if needInit(v.Type) {
 			var (
 				name     string
 				desc     string
@@ -1772,7 +1778,7 @@ func buildErrorsData(e *expr.HTTPEndpointExpr, sd *ServiceData) []*ErrorGroupDat
 					AttributeData: &AttributeData{Name: "body", VarName: "body", TypeRef: sd.Scope.GoTypeRef(v.Response.Body)},
 				}}
 			}
-			for _, h := range extractHeaders(v.Response.Headers, v.ErrorExpr.AttributeExpr, errctx, sd.Scope) {
+			for _, h := range extractHeaders(v.Response.Headers, v.AttributeExpr, errctx, sd.Scope) {
 				args = append(args, &InitArgData{
 					Ref: h.VarName,
 					AttributeData: &AttributeData{
@@ -1788,7 +1794,7 @@ func buildErrorsData(e *expr.HTTPEndpointExpr, sd *ServiceData) []*ErrorGroupDat
 					},
 				})
 			}
-			for _, c := range extractCookies(v.Response.Cookies, v.ErrorExpr.AttributeExpr, errctx, sd.Scope) {
+			for _, c := range extractCookies(v.Response.Cookies, v.AttributeExpr, errctx, sd.Scope) {
 				args = append(args, &InitArgData{
 					Ref: c.VarName,
 					AttributeData: &AttributeData{
@@ -1811,7 +1817,7 @@ func buildErrorsData(e *expr.HTTPEndpointExpr, sd *ServiceData) []*ErrorGroupDat
 				err    error
 			)
 			if body != expr.Empty {
-				eAtt := v.ErrorExpr.AttributeExpr
+				eAtt := v.AttributeExpr
 				// If design uses Body("name") syntax then need to use payload
 				// attribute to transform.
 				if o, ok := v.Response.Body.Meta["origin:attribute"]; ok {
@@ -1824,10 +1830,10 @@ func buildErrorsData(e *expr.HTTPEndpointExpr, sd *ServiceData) []*ErrorGroupDat
 				if err == nil {
 					sd.ClientTransformHelpers = codegen.AppendHelpers(sd.ClientTransformHelpers, helpers)
 				}
-			} else if expr.IsArray(v.ErrorExpr.Type) || expr.IsMap(v.ErrorExpr.Type) {
+			} else if expr.IsArray(v.Type) || expr.IsMap(v.Type) {
 				if params := expr.AsObject(e.QueryParams().Type); len(*params) > 0 {
 					var helpers []*codegen.TransformFunctionData
-					code, helpers, err = unmarshal((*params)[0].Attribute, v.ErrorExpr.AttributeExpr, codegen.Goify((*params)[0].Name, false), "v", httpclictx, errctx)
+					code, helpers, err = unmarshal((*params)[0].Attribute, v.AttributeExpr, codegen.Goify((*params)[0].Name, false), "v", httpclictx, errctx)
 					if err == nil {
 						sd.ClientTransformHelpers = codegen.AppendHelpers(sd.ClientTransformHelpers, helpers)
 					}
@@ -1841,9 +1847,9 @@ func buildErrorsData(e *expr.HTTPEndpointExpr, sd *ServiceData) []*ErrorGroupDat
 				Name:                name,
 				Description:         desc,
 				ClientArgs:          args,
-				ReturnTypeName:      svc.Scope.GoFullTypeName(v.ErrorExpr.AttributeExpr, pkg),
-				ReturnTypeRef:       svc.Scope.GoFullTypeRef(v.ErrorExpr.AttributeExpr, pkg),
-				ReturnIsStruct:      expr.IsObject(v.ErrorExpr.Type),
+				ReturnTypeName:      svc.Scope.GoFullTypeName(v.AttributeExpr, pkg),
+				ReturnTypeRef:       svc.Scope.GoFullTypeRef(v.AttributeExpr, pkg),
+				ReturnIsStruct:      expr.IsObject(v.Type),
 				ReturnTypeAttribute: codegen.Goify(origin, true),
 				ReturnTypePkg:       pkg,
 				ClientCode:          code,
@@ -1860,10 +1866,10 @@ func buildErrorsData(e *expr.HTTPEndpointExpr, sd *ServiceData) []*ErrorGroupDat
 			)
 			{
 				errorLoc := ep.ErrorLocs[v.ErrorExpr.Name]
-				if sbd := buildResponseBodyType(v.Response.Body, v.ErrorExpr.AttributeExpr, errorLoc, e, true, nil, sd); sbd != nil {
+				if sbd := buildResponseBodyType(v.Response.Body, v.AttributeExpr, errorLoc, e, true, nil, sd); sbd != nil {
 					serverBodyData = append(serverBodyData, sbd)
 				}
-				clientBodyData = buildResponseBodyType(v.Response.Body, v.ErrorExpr.AttributeExpr, errorLoc, e, false, nil, sd)
+				clientBodyData = buildResponseBodyType(v.Response.Body, v.AttributeExpr, errorLoc, e, false, nil, sd)
 				if clientBodyData != nil {
 					sd.ClientTypeNames[clientBodyData.Name] = false
 					clientBodyData.Description = fmt.Sprintf("%s is the type of the %q service %q endpoint HTTP response body for the %q error.",
@@ -1873,8 +1879,8 @@ func buildErrorsData(e *expr.HTTPEndpointExpr, sd *ServiceData) []*ErrorGroupDat
 				}
 			}
 
-			headers := extractHeaders(v.Response.Headers, v.ErrorExpr.AttributeExpr, errctx, sd.Scope)
-			cookies := extractCookies(v.Response.Cookies, v.ErrorExpr.AttributeExpr, errctx, sd.Scope)
+			headers := extractHeaders(v.Response.Headers, v.AttributeExpr, errctx, sd.Scope)
+			cookies := extractCookies(v.Response.Cookies, v.AttributeExpr, errctx, sd.Scope)
 			var mustValidate bool
 			for _, h := range headers {
 				if h.Validate != "" || h.Required || needConversion(h.Type) {
@@ -1905,7 +1911,7 @@ func buildErrorsData(e *expr.HTTPEndpointExpr, sd *ServiceData) []*ErrorGroupDat
 			}
 		}
 
-		ref := svc.Scope.GoFullTypeRef(v.ErrorExpr.AttributeExpr, pkg)
+		ref := svc.Scope.GoFullTypeRef(v.AttributeExpr, pkg)
 		data[ref] = append(data[ref], &ErrorData{
 			Name:     v.Name,
 			Response: responseData,
@@ -2770,13 +2776,8 @@ func upgradeParams(e *EndpointData, fn string) map[string]any {
 	}
 }
 
-// needStream returns true if at least one method in the defined services
-// uses stream for sending payload/result.
-func needStream(data []*ServiceData) bool {
-	for _, svc := range data {
-		if hasWebSocket(svc) {
-			return true
-		}
-	}
-	return false
+// needDialer returns true if at least one method in the defined services
+// uses WebSocket for sending payload or result.
+func needDialer(data []*ServiceData) bool {
+	return slices.ContainsFunc(data, hasWebSocket)
 }
diff --git a/http/codegen/sse.go b/http/codegen/sse.go
new file mode 100644
index 00000000..9494f1a9
--- /dev/null
+++ b/http/codegen/sse.go
@@ -0,0 +1,191 @@
+package codegen
+
+import (
+	"fmt"
+	"path/filepath"
+
+	"slices"
+
+	"goa.design/goa/v3/codegen"
+	"goa.design/goa/v3/expr"
+)
+
+type (
+	// SSEData contains the data needed to render struct type that
+	// implements the server and client stream interface for SSE.
+	SSEData struct {
+		// StructName is the name of the generated struct which encapsulates the
+		// server implementation.
+		StructName string
+		// Interface is the fully qualified name of the interface that
+		// the struct implements.
+		Interface string
+		// SendName is the name of the send function.
+		SendName string
+		// SendDesc is the description for the send function.
+		SendDesc string
+		// SendWithContextName is the name of the send function with context.
+		SendWithContextName string
+		// SendWithContextDesc is the description for the send function with context.
+		SendWithContextDesc string
+		// RecvName is the name of the client method to connect to the SSE endpoint.
+		RecvName string
+		// RecvDesc is the description for the client method.
+		RecvDesc string
+		// EventTypeRef is the fully qualified type ref for the event type.
+		EventTypeRef string
+		// EventTypeName is the name of the event type without package qualifier.
+		EventTypeName string
+		// EventIsStruct indicates whether the SSE method return type is a struct.
+		EventIsStruct bool
+		// DataFieldTypeRef is the fully qualified type ref for the data field if any.
+		DataFieldTypeRef string
+		// DataField is the name of the result type event data attribute if any.
+		// If empty, the entire result type is used as the data field.
+		DataField string
+		// IDField is the name of the result type event ID attribute if any.
+		// If empty, no id field is included in the event.
+		IDField string
+		// EventField is the name of the result type event field if any.
+		// If empty, no event field is included in the event.
+		EventField string
+		// RetryField is the name of the result type event retry field if any.
+		// If empty, no retry field is included in the event.
+		RetryField string
+		// RequestIDField is the name of the payload field that maps to the Last-Event-ID header if any.
+		// If empty, no last event id is included in the request.
+		RequestIDField string
+	}
+)
+
+// initSSEData initializes the SSE related data in ed.
+func initSSEData(ed *EndpointData, e *expr.HTTPEndpointExpr, sd *ServiceData) {
+	if e.SSE == nil {
+		return
+	}
+
+	md := ed.Method
+	svc := sd.Service
+	sendDesc := fmt.Sprintf("%s streams instances of %q to the %q endpoint SSE connection.", md.ServerStream.SendName, ed.Result.Name, md.Name)
+	sendWithContextDesc := fmt.Sprintf("%s streams instances of %q to the %q endpoint SSE connection with context.", md.ServerStream.SendWithContextName, ed.Result.Name, md.Name)
+	recvDesc := fmt.Sprintf("%s connects to the %q SSE endpoint and streams events.", md.ServerStream.RecvName, md.Name)
+
+	var dataFieldTypeRef string
+	if e.SSE.DataField != "" {
+		if obj, ok := e.MethodExpr.Result.Type.(*expr.Object); ok {
+			for _, nat := range *obj {
+				if nat.Name == e.SSE.DataField {
+					dataFieldTypeRef = sd.Service.Scope.GoFullTypeRef(nat.Attribute, svc.PkgName)
+					break
+				}
+			}
+		}
+	}
+
+	ed.SSE = &SSEData{
+		StructName:          md.ServerStream.VarName,
+		Interface:           fmt.Sprintf("%s.%s", svc.PkgName, md.ServerStream.Interface),
+		SendName:            md.ServerStream.SendName,
+		SendDesc:            sendDesc,
+		SendWithContextName: md.ServerStream.SendWithContextName,
+		SendWithContextDesc: sendWithContextDesc,
+		RecvName:            md.ClientStream.RecvName,
+		RecvDesc:            recvDesc,
+		EventTypeRef:        ed.Result.Ref,
+		EventTypeName:       ed.Result.Name,
+		EventIsStruct:       ed.Result.IsStruct,
+		DataFieldTypeRef:    dataFieldTypeRef,
+		DataField:           e.SSE.DataField,
+		IDField:             e.SSE.IDField,
+		EventField:          e.SSE.EventField,
+		RetryField:          e.SSE.RetryField,
+		RequestIDField:      e.SSE.RequestIDField,
+	}
+}
+
+// sseServerFile returns the file implementing the SSE server
+// streaming implementation if any.
+func sseServerFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.File {
+	data := HTTPServices.Get(svc.Name())
+	if data == nil {
+		return nil
+	}
+
+	// Check if any endpoint has SSE
+	hasSSE := false
+	for _, ed := range data.Endpoints {
+		if ed.SSE != nil {
+			hasSSE = true
+			break
+		}
+	}
+	if !hasSSE {
+		return nil
+	}
+
+	path := filepath.Join(codegen.Gendir, "http", codegen.SnakeCase(svc.Name()), "server", "sse.go")
+	sections := []*codegen.SectionTemplate{
+		codegen.Header(
+			"sse",
+			"server",
+			[]*codegen.ImportSpec{
+				{Path: "context"},
+				{Path: "io"},
+				{Path: "net/http"},
+				{Path: "sync"},
+				{Path: "time"},
+				{Path: "encoding/json"},
+				{Path: "fmt"},
+				{Path: genpkg + "/" + codegen.SnakeCase(svc.Name())},
+				{Path: genpkg + "/" + codegen.SnakeCase(svc.Name()) + "/views"},
+			},
+		),
+	}
+	sections = append(sections, sseTemplateSections(data)...)
+	return &codegen.File{Path: path, SectionTemplates: sections}
+}
+
+// sseTemplateSections returns section templates for SSE endpoints.
+func sseTemplateSections(data *ServiceData) []*codegen.SectionTemplate {
+	sections := make([]*codegen.SectionTemplate, 0)
+	for _, ed := range data.Endpoints {
+		if ed.SSE == nil {
+			continue
+		}
+		// Create a map of template functions needed for the SSE template
+		funcs := map[string]interface{}{
+			"dict": func(values ...any) (map[string]any, error) {
+				if len(values)%2 != 0 {
+					return nil, fmt.Errorf("odd number of arguments")
+				}
+				dict := make(map[string]any, len(values)/2)
+				for i := 0; i < len(values); i += 2 {
+					key, ok := values[i].(string)
+					if !ok {
+						return nil, fmt.Errorf("dict keys must be strings")
+					}
+					dict[key] = values[i+1]
+				}
+				return dict, nil
+			},
+		}
+		sections = append(sections, &codegen.SectionTemplate{
+			Name:    "server-sse",
+			Source:  readTemplate("server_sse", "sse_format"),
+			Data:    ed,
+			FuncMap: funcs,
+		})
+	}
+	return sections
+}
+
+// isSSEEndpoint returns true if the endpoint defines a streaming result
+// with SSE.
+func isSSEEndpoint(ed *EndpointData) bool {
+	return ed.SSE != nil
+}
+
+// hasSSE returns true if at least one endpoint in the service uses SSE.
+func hasSSE(data *ServiceData) bool {
+	return slices.ContainsFunc(data.Endpoints, isSSEEndpoint)
+}
diff --git a/http/codegen/sse_client.go b/http/codegen/sse_client.go
new file mode 100644
index 00000000..5852e932
--- /dev/null
+++ b/http/codegen/sse_client.go
@@ -0,0 +1,85 @@
+package codegen
+
+import (
+	"fmt"
+	"path/filepath"
+
+	"goa.design/goa/v3/codegen"
+	"goa.design/goa/v3/expr"
+)
+
+// sseClientFile returns the file implementing the SSE client code for SSE endpoints if any.
+// Relies on SSEData (ed.SSE) for all codegen needs.
+func sseClientFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.File {
+	data := HTTPServices.Get(svc.Name())
+	if data == nil {
+		return nil
+	}
+	// Check if any endpoint has SSE
+	hasSSE := false
+	for _, ed := range data.Endpoints {
+		if ed.SSE != nil {
+			hasSSE = true
+			break
+		}
+	}
+	if !hasSSE {
+		return nil
+	}
+	path := filepath.Join(codegen.Gendir, "http", codegen.SnakeCase(svc.Name()), "client", "sse.go")
+	sections := []*codegen.SectionTemplate{
+		codegen.Header(
+			"sse-client",
+			"client",
+			[]*codegen.ImportSpec{
+				{Path: "bytes"},
+				{Path: "context"},
+				{Path: "encoding/json"},
+				{Path: "io"},
+				{Path: "net/http"},
+				{Path: "fmt"},
+				{Path: "strings"},
+				{Path: "strconv"},
+				{Path: "sync"},
+				{Path: genpkg + "/" + codegen.SnakeCase(svc.Name())},
+				{Path: genpkg + "/" + codegen.SnakeCase(svc.Name()) + "/views"},
+			},
+		),
+	}
+	sections = append(sections, sseClientTemplateSections(data)...) // add SSE client methods
+	return &codegen.File{Path: path, SectionTemplates: sections}
+}
+
+// sseClientTemplateSections returns section templates for SSE client endpoints.
+func sseClientTemplateSections(data *ServiceData) []*codegen.SectionTemplate {
+	sections := make([]*codegen.SectionTemplate, 0)
+	for _, ed := range data.Endpoints {
+		if ed.SSE == nil {
+			continue
+		}
+		// Create a map of template functions needed for the SSE template
+		funcs := map[string]interface{}{
+			"dict": func(values ...any) (map[string]any, error) {
+				if len(values)%2 != 0 {
+					return nil, fmt.Errorf("odd number of arguments")
+				}
+				dict := make(map[string]any, len(values)/2)
+				for i := 0; i < len(values); i += 2 {
+					key, ok := values[i].(string)
+					if !ok {
+						return nil, fmt.Errorf("dict keys must be strings")
+					}
+					dict[key] = values[i+1]
+				}
+				return dict, nil
+			},
+		}
+		sections = append(sections, &codegen.SectionTemplate{
+			Name:    "client-sse",
+			Source:  readTemplate("client_sse", "sse_parse"),
+			Data:    ed,
+			FuncMap: funcs,
+		})
+	}
+	return sections
+}
diff --git a/http/codegen/templates/cli_end.go.tpl b/http/codegen/templates/cli_end.go.tpl
index 1a006c4c..bb31b97d 100644
--- a/http/codegen/templates/cli_end.go.tpl
+++ b/http/codegen/templates/cli_end.go.tpl
@@ -5,7 +5,7 @@ return cli.ParseEndpoint(
 		goahttp.RequestEncoder,
 		goahttp.ResponseDecoder,
 		debug,
-{{- if needStream .Services }}
+{{- if needDialer .Services }}
 		dialer,
 	{{- range $svc := .Services }}
 		{{- if hasWebSocket $svc }}
diff --git a/http/codegen/templates/cli_streaming.go.tpl b/http/codegen/templates/cli_streaming.go.tpl
index a1cb16b4..076e22c2 100644
--- a/http/codegen/templates/cli_streaming.go.tpl
+++ b/http/codegen/templates/cli_streaming.go.tpl
@@ -1,4 +1,4 @@
-{{- if needStream .Services }}
+{{- if needDialer .Services }}
 	var (
     dialer *websocket.Dialer
   )
diff --git a/http/codegen/templates/client_sse.go.tpl b/http/codegen/templates/client_sse.go.tpl
new file mode 100644
index 00000000..e767c433
--- /dev/null
+++ b/http/codegen/templates/client_sse.go.tpl
@@ -0,0 +1,245 @@
+type (
+	// {{ .Method.VarName }}StreamImpl implements the {{ .ServiceName }}.{{ .Method.VarName }}ClientStream interface.
+	{{ .Method.VarName }}StreamImpl struct {
+		resp *http.Response
+		buffer []byte // Buffer for unprocessed data
+		lock sync.Mutex
+		closed bool
+	}
+)
+
+// {{ .Method.VarName }}StreamImpl implements the {{ .ServiceName }}.{{ .Method.VarName }}ClientStream interface.
+var _ {{ .ServiceName }}.{{ .Method.VarName }}ClientStream = (*{{ .Method.VarName }}StreamImpl)(nil)
+
+// New{{ .Method.VarName }}Stream creates a new {{ .ServiceName }}.{{ .Method.VarName }}ClientStream.
+func New{{ .Method.VarName }}Stream(resp *http.Response) {{ .ServiceName }}.{{ .Method.VarName }}ClientStream {
+	return &{{ .Method.VarName }}StreamImpl{
+		resp: resp,
+		buffer: make([]byte, 0, 4096), // Pre-allocate buffer
+	}
+}
+
+// Recv reads and returns the next event from the SSE stream.
+func (s *{{ .Method.VarName }}StreamImpl) Recv() (event {{ .SSE.EventTypeRef }}, err error) {
+	return s.RecvWithContext(context.Background())
+}
+
+// RecvWithContext reads and returns the next event from the SSE stream, respecting context cancellation.
+func (s *{{ .Method.VarName }}StreamImpl) RecvWithContext(ctx context.Context) (event {{ .SSE.EventTypeRef }}, err error) {
+	var byts []byte
+	byts, err = s.readEvent(ctx)
+	if err != nil {
+		if err == io.EOF || err == context.Canceled || err == context.DeadlineExceeded {
+			// Clean up on EOF or context cancellation
+			s.Close()
+			if err == io.EOF {
+				err = nil
+			}
+		}
+		return
+	}
+	return s.processEvent(byts)
+}
+
+// readEvent reads a single SSE event from the stream, respecting context
+// cancellation.  It first checks the internal buffer for a complete event
+// (delimited by double newlines). If no complete event is found, it reads from
+// the HTTP response body until it either finds an event boundary, reaches EOF,
+// or encounters an error. Any data after the event boundary is saved in the
+// buffer for the next call.
+func (s *{{ .Method.VarName }}StreamImpl) readEvent(ctx context.Context) ([]byte, error) {
+	const bufSize = 4096 // 4KB buffer size
+
+	// Check for event in existing buffer
+	event, ok := s.checkBuffer()
+	if ok {
+		return event, nil
+	}
+
+	// Initialize with any data from buffer
+	eventData := event
+	wasNewline := len(eventData) > 0 && eventData[len(eventData)-1] == '\n'
+	buf := make([]byte, bufSize)
+
+	// Read data in chunks until we find an event or hit EOF
+	for {
+		// Check if context is done
+		select {
+		case <-ctx.Done():
+			if len(eventData) > 0 {
+				return eventData, nil
+			}
+			return nil, ctx.Err()
+		default:
+			// Continue processing
+		}
+
+		// Check if stream is closed
+		s.lock.Lock()
+		if s.closed {
+			s.lock.Unlock()
+			if len(eventData) > 0 {
+				return eventData, nil
+			}
+			return nil, io.EOF
+		}
+
+		// Read next chunk
+		n, err := s.resp.Body.Read(buf)
+		s.lock.Unlock()
+
+		// Handle read errors
+		if err != nil && err != io.EOF {
+			return nil, err
+		}
+
+		// Process data if we got any
+		if n > 0 {
+			// Look for event boundary in this chunk
+			for i := 0; i < n; i++ {
+				b := buf[i]
+				eventData = append(eventData, b)
+
+				// Check for double newlines (event boundary)
+				if b == '\n' && wasNewline {
+					// Save any remaining data for next read
+					if i+1 < n {
+						s.lock.Lock()
+						s.buffer = append(s.buffer[:0], buf[i+1:n]...)
+						s.lock.Unlock()
+					}
+					return eventData, nil
+				}
+
+				// Update newline tracking
+				wasNewline = (b == '\n')
+			}
+		}
+
+		// Return partial data at EOF
+		if err == io.EOF {
+			if len(eventData) > 0 {
+				return eventData, nil
+			}
+			return nil, io.EOF
+		}
+	}
+}
+
+// checkBuffer examines the internal buffer for a complete SSE event (delimited
+// by double newlines).  It returns two values: the event data (or all buffer
+// contents if no complete event is found), and a boolean indicating whether a
+// complete event was found. If a complete event is found, any remaining data
+// after the event is kept in the buffer for the next call.
+func (s *{{ .Method.VarName }}StreamImpl) checkBuffer() ([]byte, bool) {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+
+	// Quick return if buffer is empty
+	if len(s.buffer) == 0 {
+		return nil, false
+	}
+
+	// Look for double newline in buffer
+	for i := 0; i < len(s.buffer)-1; i++ {
+		if s.buffer[i] == '\n' && s.buffer[i+1] == '\n' {
+			// Found complete event
+			eventEnd := i + 2 // Include both newlines
+			eventData := s.buffer[:eventEnd]
+
+			// Save remaining data for next time
+			if eventEnd < len(s.buffer) {
+				s.buffer = append(s.buffer[:0], s.buffer[eventEnd:]...)
+			} else {
+				s.buffer = s.buffer[:0]
+			}
+
+			return eventData, true
+		}
+	}
+
+	// No complete event found, return buffer contents
+	eventData := s.buffer
+	s.buffer = s.buffer[:0] // Clear buffer but keep capacity
+	return eventData, false
+}
+
+// Close closes the SSE stream and releases any associated resources.
+func (s *{{ .Method.VarName }}StreamImpl) Close() error {
+	s.lock.Lock()
+	defer s.lock.Unlock()
+	if s.closed {
+		return nil
+	}
+	s.closed = true
+	return s.resp.Body.Close()
+}
+
+// processEvent processes a raw SSE event into the expected type
+func (s *{{ .Method.VarName }}StreamImpl) processEvent(eventData []byte) (event {{ .SSE.EventTypeRef }}, err error) {
+	{{- if .SSE.EventIsStruct }}
+	event = new({{ .SSE.EventTypeName }})
+	{{- end }}
+	{{- if .SSE.IDField }}
+	var id string
+	{{- end }}
+	{{- if .SSE.EventField }}
+	var eventType string
+	{{- end }}
+	{{- if .SSE.RetryField }}
+	var retry int
+	{{- end }}
+	var dataLines []string
+	for _, line := range bytes.Split(eventData, []byte("\n")) {
+		if len(line) == 0 {
+			continue
+		}
+		if bytes.HasPrefix(line, []byte("data:")) {
+			dataLines = append(dataLines, s.trimHeader(len("data:"), line))
+			continue
+		}
+
+		{{- if .SSE.IDField }}
+		if bytes.HasPrefix(line, []byte("id:")) {
+			event.{{ .SSE.IDField }} = s.trimHeader(len("id:"), line)
+			continue
+		}
+		{{- end }}
+		
+		{{- if .SSE.EventField }}
+		if bytes.HasPrefix(line, []byte("event:")) {
+			event.{{ .SSE.EventField }} = s.trimHeader(len("event:"), line)
+			continue
+		}
+		{{- end }}
+		
+		{{- if .SSE.RetryField }}
+		if bytes.HasPrefix(line, []byte("retry:")) {
+			event.{{ .SSE.RetryField }} = s.trimHeader(len("retry:"), line)
+			continue
+		}
+		{{- end }}
+	}
+	if len(dataLines) > 0 {
+		dataContent := strings.Join(dataLines, "\n")
+		{{- if .SSE.DataField }}
+		{{ template "partial_sse_parse" dict "Target" (printf "event.%s" .SSE.DataField) "TypeRef" .SSE.DataFieldTypeRef }}
+		{{- else }}
+		{{ template "partial_sse_parse" dict "Target" "event" "TypeRef" .SSE.EventTypeRef }}
+		{{- end }}
+	}
+
+	return
+}
+
+// trimHeader removes the header prefix and optional leading space
+func (s *{{ .Method.VarName }}StreamImpl) trimHeader(size int, data []byte) string {
+	if len(data) < size {
+		return string(data)
+	}
+	data = data[size:]
+	if len(data) > 0 && data[0] == ' ' {
+		data = data[1:]
+	}
+	return string(data)
+}
diff --git a/http/codegen/templates/endpoint_init.go.tpl b/http/codegen/templates/endpoint_init.go.tpl
index b0f117db..e2c5ad0a 100644
--- a/http/codegen/templates/endpoint_init.go.tpl
+++ b/http/codegen/templates/endpoint_init.go.tpl
@@ -8,7 +8,9 @@ func (c *{{ .ClientStruct }}) {{ .EndpointInit }}({{ if .MultipartRequestEncoder
 		encodeRequest  = {{ .RequestEncoder }}({{ if .MultipartRequestEncoder }}{{ .MultipartRequestEncoder.InitName }}({{ .MultipartRequestEncoder.VarName }}){{ else }}c.encoder{{ end }})
 			{{- end }}
 		{{- end }}
+		{{- if not (isSSEEndpoint .) }}
 		decodeResponse = {{ .ResponseDecoder }}(c.decoder, c.RestoreResponseBody)
+		{{- end }}
 	)
 	return func(ctx context.Context, v any) (any, error) {
 		req, err := c.{{ .RequestInit.Name }}(ctx, {{ range .RequestInit.ClientArgs }}{{ .Ref }}, {{ end }})
@@ -58,6 +60,25 @@ func (c *{{ .ClientStruct }}) {{ .EndpointInit }}({{ if .MultipartRequestEncoder
 			{{- end }}
 		{{- end }}
 		return stream, nil
+	{{- else if isSSEEndpoint . }}
+		// For SSE endpoints, connect and return a stream
+		resp, err := c.{{ .Method.VarName }}Doer.Do(req)
+		if err != nil {
+			return nil, goahttp.ErrRequestError("{{ .ServiceName }}", "{{ .Method.Name }}", err)
+		}
+		
+		if resp.StatusCode != http.StatusOK {
+			resp.Body.Close()
+			return nil, fmt.Errorf("unexpected status from SSE endpoint: %d", resp.StatusCode)
+		}
+		
+		contentType := resp.Header.Get("Content-Type")
+		if contentType != "" && !strings.HasPrefix(contentType, "text/event-stream") {
+			resp.Body.Close()
+			return nil, fmt.Errorf("unexpected content type: %s (expected text/event-stream)", contentType)
+		}
+		
+		return New{{ .Method.VarName }}Stream(resp), nil
 	{{- else }}
 		resp, err := c.{{ .Method.VarName }}Doer.Do(req)
 		if err != nil {
diff --git a/http/codegen/templates/parse_endpoint.go.tpl b/http/codegen/templates/parse_endpoint.go.tpl
index 55ef1735..16f1ab16 100644
--- a/http/codegen/templates/parse_endpoint.go.tpl
+++ b/http/codegen/templates/parse_endpoint.go.tpl
@@ -9,7 +9,7 @@ func ParseEndpoint(
 	{{- if streamingCmdExists .Commands }}
 	dialer goahttp.Dialer,
 		{{- range .Commands }}
-			{{- if .NeedStream }}
+			{{- if .NeedDialer }}
 				{{ .VarName }}Configurer *{{ .PkgName }}.ConnConfigurer,
 			{{- end }}
 		{{- end }}
@@ -35,7 +35,7 @@ func ParseEndpoint(
 		switch svcn {
 	{{- range .Commands }}
 		case "{{ .Name }}":
-			c := {{ .PkgName }}.NewClient(scheme, host, doer, enc, dec, restore{{ if .NeedStream }}, dialer, {{ .VarName }}Configurer{{ end }})
+			c := {{ .PkgName }}.NewClient(scheme, host, doer, enc, dec, restore{{ if .NeedDialer }}, dialer, {{ .VarName }}Configurer{{ end }})
 			switch epn {
 		{{- $pkgName := .PkgName }}
 		{{- range .Subcommands }}
diff --git a/http/codegen/templates/partial/sse_format.go.tpl b/http/codegen/templates/partial/sse_format.go.tpl
new file mode 100644
index 00000000..1745c219
--- /dev/null
+++ b/http/codegen/templates/partial/sse_format.go.tpl
@@ -0,0 +1,21 @@
+{{- if eq .TypeRef "string" }}
+	data = {{ .VarName }}
+{{- else if eq .TypeRef "boolean" }}
+	if {{ .VarName }} {
+		data = "true"
+	} else {
+		data = "false"
+	}
+{{- else if eq .TypeRef "bytes" }}
+	data = string({{ .VarName }})
+{{- else if or (eq .TypeRef "int") (eq .TypeRef "int32") (eq .TypeRef "int64") (eq .TypeRef "uint") (eq .TypeRef "uint32") (eq .TypeRef "uint64") }}
+	data = fmt.Sprintf("%d", {{ .VarName }})
+{{- else if or (eq .TypeRef "float32") (eq .TypeRef "float64") }}
+	data = fmt.Sprintf("%g", {{ .VarName }})
+{{- else }}
+	byts, err := json.Marshal({{ .VarName }})
+	if err != nil {
+		return err
+	}
+	data = string(byts)
+{{- end }}
\ No newline at end of file
diff --git a/http/codegen/templates/partial/sse_parse.go.tpl b/http/codegen/templates/partial/sse_parse.go.tpl
new file mode 100644
index 00000000..b9eb5245
--- /dev/null
+++ b/http/codegen/templates/partial/sse_parse.go.tpl
@@ -0,0 +1,53 @@
+{{- if eq .TypeRef "string" }}
+	{{ .Target }} = dataContent
+{{- else if eq .TypeRef "boolean" }}
+	var val bool
+	val, err = strconv.ParseBool(dataContent)
+	if err != nil {
+		return
+	}
+	{{ .Target }} = val
+{{- else if eq .TypeRef "bytes" }}
+	{{ .Target }} = []byte(dataContent)
+{{- else if or (eq .TypeRef "int") (eq .TypeRef "int32") }}
+	var val int64
+	val, err = strconv.ParseInt(dataContent, 10, 0)
+	if err != nil {
+		return
+	}
+	{{ .Target }} = {{ .TypeRef }}(val)
+{{- else if eq .TypeRef "int64" }}
+	{{ .Target }}, err = strconv.ParseInt(dataContent, 10, 64)
+	if err != nil {
+		return
+	}
+{{- else if or (eq .TypeRef "uint") (eq .TypeRef "uint32") }}
+	var val uint64
+	val, err = strconv.ParseUint(dataContent, 10, 0)
+	if err != nil {
+		return
+	}
+	{{ .Target }} = {{ .TypeRef }}(val)
+{{- else if eq .TypeRef "uint64" }}
+	{{ .Target }}, err = strconv.ParseUint(dataContent, 10, 64)
+	if err != nil {
+		return
+	}
+{{- else if eq .TypeRef "float32" }}
+	var val float64
+	val, err = strconv.ParseFloat(dataContent, 32)
+	if err != nil {
+		return
+	}
+	{{ .Target }} = float32(val)
+{{- else if eq .TypeRef "float64" }}
+	{{ .Target }}, err = strconv.ParseFloat(dataContent, 64)
+	if err != nil {
+		return
+	}
+{{- else }}
+	err = json.Unmarshal([]byte(dataContent), &{{ .Target }})
+	if err != nil {
+		return
+	}
+{{- end }}
\ No newline at end of file
diff --git a/http/codegen/templates/request_init.go.tpl b/http/codegen/templates/request_init.go.tpl
index 09e19138..c6306197 100644
--- a/http/codegen/templates/request_init.go.tpl
+++ b/http/codegen/templates/request_init.go.tpl
@@ -45,7 +45,7 @@
 		}
 		body = rd.Body
 {{- end }}
-	{{- if .IsStreaming }}
+	{{- if .IsWebSocket }}
 		scheme := c.scheme
 		switch c.scheme {
 		case "http":
@@ -54,7 +54,7 @@
 			scheme = "wss"
 		}
 	{{- end }}
-	u := &url.URL{Scheme: {{ if .IsStreaming }}scheme{{ else }}c.scheme{{ end }}, Host: c.host, Path: {{ .PathInit.Name }}({{ range .Args }}{{ .Ref }}, {{ end }})}
+	u := &url.URL{Scheme: {{ if .IsWebSocket }}scheme{{ else }}c.scheme{{ end }}, Host: c.host, Path: {{ .PathInit.Name }}({{ range .Args }}{{ .Ref }}, {{ end }})}
 	req, err := http.NewRequest("{{ .Verb }}", u.String(), {{ if .RequestStruct }}body{{ else }}nil{{ end }})
 	if err != nil {
 		return nil, goahttp.ErrInvalidURL("{{ .ServiceName }}", "{{ .EndpointName }}", u.String(), err)
diff --git a/http/codegen/templates/response_encoder.go.tpl b/http/codegen/templates/response_encoder.go.tpl
index 1500adf4..5b5ab0b5 100644
--- a/http/codegen/templates/response_encoder.go.tpl
+++ b/http/codegen/templates/response_encoder.go.tpl
@@ -33,7 +33,9 @@ func {{ .ResponseEncoder }}(encoder func(context.Context, http.ResponseWriter) g
 		{{- end }}
 	{{- else }}
 		{{- with (index .Result.Responses 0) }}
-			w.WriteHeader({{ .StatusCode }})
+			{{- if not $.Method.SkipResponseBodyEncodeDecode }}
+				w.WriteHeader({{ .StatusCode }})
+			{{- end }}
 			return nil
 		{{- end }}
 	{{- end }}
diff --git a/http/codegen/templates/server_configure.go.tpl b/http/codegen/templates/server_configure.go.tpl
index 1f186a9a..c337e9e6 100644
--- a/http/codegen/templates/server_configure.go.tpl
+++ b/http/codegen/templates/server_configure.go.tpl
@@ -10,7 +10,7 @@
 	)
 	{
 		eh := errorHandler(ctx)
-	{{- if needStream .Services }}
+	{{- if needDialer .Services }}
 		upgrader := &websocket.Upgrader{}
 	{{- end }}
 	{{- range $svc := .Services }}
diff --git a/http/codegen/templates/server_handler_init.go.tpl b/http/codegen/templates/server_handler_init.go.tpl
index fdd3a452..cd2b4512 100644
--- a/http/codegen/templates/server_handler_init.go.tpl
+++ b/http/codegen/templates/server_handler_init.go.tpl
@@ -11,19 +11,19 @@ func {{ .HandlerInit }}(
 	configurer goahttp.ConnConfigureFunc,
 	{{- end }}
 ) http.Handler {
-	{{- if (or (mustDecodeRequest .) (not (or .Redirect (isWebSocketEndpoint .))) (not .Redirect) .Method.SkipResponseBodyEncodeDecode) }}
+	{{- if (or (mustDecodeRequest .) (not (or .Redirect (isWebSocketEndpoint .) (isSSEEndpoint .))) (not .Redirect) .Method.SkipResponseBodyEncodeDecode) }}
 	var (
 	{{- end }}
 		{{- if mustDecodeRequest . }}
 		decodeRequest  = {{ .RequestDecoder }}(mux, decoder)
 		{{- end }}
-		{{- if not (or .Redirect (isWebSocketEndpoint .)) }}
+		{{- if not (or .Redirect (isWebSocketEndpoint .) (isSSEEndpoint .)) }}
 		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) }}
+	{{- if (or (mustDecodeRequest .) (not (or .Redirect (isWebSocketEndpoint .) (isSSEEndpoint .))) (not .Redirect) .Method.SkipResponseBodyEncodeDecode) }}
 	)
 	{{- end }}
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -58,6 +58,30 @@ func {{ .HandlerInit }}(
 		{{- end }}
 		}
 		_, err = endpoint(ctx, v)
+	{{- else if isSSEEndpoint . }}
+		{{- if .SSE.RequestIDField }}
+		// Set Last-Event-ID header if present
+		if lastEventID := r.Header.Get("Last-Event-ID"); lastEventID != "" {
+			ctx = context.WithValue(ctx, "last-event-id", lastEventID)
+			{{- if .Payload.Ref }}
+			{{- if eq .Method.Payload.Type.Name "Object" }}
+			p := payload.({{ .Payload.Ref }})
+			p.{{ .SSE.RequestIDField }} = lastEventID
+			payload = p
+			{{- end }}
+			{{- end }}
+		}
+		{{- end }}
+		v := &{{ .ServicePkgName }}.{{ .Method.ServerStream.EndpointStruct }}{
+			Stream: &{{ .SSE.StructName }}{
+				w: w,
+				r: r,
+			},
+		{{- if .Payload.Ref }}
+			Payload: payload.({{ .Payload.Ref }}),
+		{{- end }}
+		}
+		_, err = endpoint(ctx, v)
 	{{- else if .Method.SkipRequestBodyEncodeDecode }}
 		data := &{{ .ServicePkgName }}.{{ .Method.RequestStruct }}{ {{ if .Payload.Ref }}Payload: payload.({{ .Payload.Ref }}), {{ end }}Body: r.Body }
 		res, err := endpoint(ctx, data)
@@ -115,7 +139,7 @@ func {{ .HandlerInit }}(
 			return
 		}
 	{{- end }}
-	{{- if not (or .Redirect (isWebSocketEndpoint .)) }}
+	{{- if not (or .Redirect (isWebSocketEndpoint .) (isSSEEndpoint .)) }}
 		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/templates/server_sse.go.tpl b/http/codegen/templates/server_sse.go.tpl
new file mode 100644
index 00000000..2135c235
--- /dev/null
+++ b/http/codegen/templates/server_sse.go.tpl
@@ -0,0 +1,78 @@
+{{ printf "%s implements the %s interface using Server-Sent Events." .SSE.StructName .SSE.Interface | comment }}
+type {{ .SSE.StructName }} struct {
+	{{ comment "once ensures the headers are written once." }}
+	once sync.Once
+	{{ comment "w is the HTTP response writer used to send the SSE events." }}
+	w http.ResponseWriter
+	{{ comment "r is the HTTP request." }}
+	r *http.Request
+}
+
+{{ printf "%s %s" .SSE.SendName .SSE.SendDesc | comment }}
+func (s *{{ .SSE.StructName }}) {{ .SSE.SendName }}(v {{ .SSE.EventTypeRef }}) error {
+	return s.{{ .SSE.SendWithContextName }}(context.Background(), v)
+}
+
+{{ printf "%s %s" .SSE.SendWithContextName .SSE.SendWithContextDesc | comment }}
+func (s *{{ .SSE.StructName }}) {{ .SSE.SendWithContextName }}(ctx context.Context, v {{ .SSE.EventTypeRef }}) error {
+	s.once.Do(func() {
+		header := s.w.Header()
+		if header.Get("Content-Type") == "" {
+			header.Set("Content-Type", "text/event-stream")
+		}
+		if header.Get("Cache-Control") == "" {
+			header.Set("Cache-Control", "no-cache")
+		}
+		if header.Get("Connection") == "" {
+			header.Set("Connection", "keep-alive")
+		}
+		s.w.WriteHeader(http.StatusOK)
+	})
+
+	{{- if .Method.ViewedResult }}
+		{{- if .Method.ViewedResult.ViewName }}
+	res := {{ .Service.PkgName }}.{{ .Method.ViewedResult.Init.Name }}(v, {{ printf "%q" .Method.ViewedResult.ViewName }})
+		{{- else }}
+	res := {{ .Service.PkgName }}.{{ .Method.ViewedResult.Init.Name }}(v, "default")
+		{{- end }}
+	{{- else }}
+	res := v
+	{{- end }}
+	
+	{{ if .SSE.IDField }}
+	if id := res.{{ .SSE.IDField }}; id != "" {
+		fmt.Fprintf(s.w, "id: %s\n", id)
+	}
+	{{- end }}
+
+	{{- if .SSE.EventField }}
+	if event := res.{{ .SSE.EventField }}; event != "" {
+		fmt.Fprintf(s.w, "event: %s\n", event)
+	}
+	{{- end }}
+
+	{{- if .SSE.RetryField }}
+	if retry := res.{{ .SSE.RetryField }}; retry > 0 {
+		fmt.Fprintf(s.w, "retry: %d\n", retry)
+	}
+	{{- end }}
+
+	var data string
+	{{- if .SSE.DataField }}
+		dataField := res.{{ .SSE.DataField }}
+		{{- template "partial_sse_format" dict "TypeRef" .SSE.DataFieldTypeRef "VarName" "dataField" }}
+	{{- else }}
+		{{- template "partial_sse_format" dict "TypeRef" .SSE.EventTypeRef "VarName" "res" }}
+	{{- end }}
+	fmt.Fprintf(s.w, "data: %s\n\n", data)
+
+	if f, ok := s.w.(http.Flusher); ok {
+		f.Flush()
+	}
+	return nil
+}
+
+{{ comment "Close is a no-op for SSE. We keep the method for compatibility with other stream types." }}
+func (s *{{ .SSE.StructName }}) Close() error {
+	return nil
+}
diff --git a/http/codegen/websocket.go b/http/codegen/websocket.go
index b0df1760..d4304cc3 100644
--- a/http/codegen/websocket.go
+++ b/http/codegen/websocket.go
@@ -3,6 +3,7 @@ package codegen
 import (
 	"fmt"
 	"path/filepath"
+	"slices"
 	"strings"
 
 	"goa.design/goa/v3/codegen"
@@ -74,6 +75,9 @@ type (
 
 // initWebSocketData initializes the WebSocket related data in ed.
 func initWebSocketData(ed *EndpointData, e *expr.HTTPEndpointExpr, sd *ServiceData) {
+	if e.SSE != nil {
+		return
+	}
 	var (
 		svrRecvTypeName        string
 		svrRecvTypeRef         string
@@ -451,12 +455,7 @@ func clientWSSections(data *ServiceData) []*codegen.SectionTemplate {
 // hasWebSocket returns true if at least one of the endpoints in the service
 // defines a streaming payload or result.
 func hasWebSocket(sd *ServiceData) bool {
-	for _, e := range sd.Endpoints {
-		if isWebSocketEndpoint(e) {
-			return true
-		}
-	}
-	return false
+	return slices.ContainsFunc(sd.Endpoints, isWebSocketEndpoint)
 }
 
 // isWebSocketEndpoint returns true if the endpoint defines a streaming payload
diff --git a/http/middleware/debug.go b/http/middleware/debug.go
index ad7c5303..afc5e467 100644
--- a/http/middleware/debug.go
+++ b/http/middleware/debug.go
@@ -37,7 +37,7 @@ func Debug(mux goahttp.Muxer, w io.Writer) func(http.Handler) http.Handler {
 			}
 
 			// Request URL
-			buf.WriteString(fmt.Sprintf("> [%s] %s %s", reqID, r.Method, r.URL.String()))
+			fmt.Fprintf(buf, "> [%s] %s %s", reqID, r.Method, r.URL.String())
 
 			// Request Headers
 			keys := make([]string, len(r.Header))
@@ -48,7 +48,7 @@ func Debug(mux goahttp.Muxer, w io.Writer) func(http.Handler) http.Handler {
 			}
 			sort.Strings(keys)
 			for _, k := range keys {
-				buf.WriteString(fmt.Sprintf("\n> [%s] %s: %s", reqID, k, strings.Join(r.Header[k], ", ")))
+				fmt.Fprintf(buf, "\n> [%s] %s: %s", reqID, k, strings.Join(r.Header[k], ", "))
 			}
 
 			// Request parameters
@@ -61,7 +61,7 @@ func Debug(mux goahttp.Muxer, w io.Writer) func(http.Handler) http.Handler {
 			}
 			sort.Strings(keys)
 			for _, k := range keys {
-				buf.WriteString(fmt.Sprintf("\n> [%s] %s: %s", reqID, k, params[k]))
+				fmt.Fprintf(buf, "\n> [%s] %s: %s", reqID, k, params[k])
 			}
 
 			// Request body
@@ -73,7 +73,7 @@ func Debug(mux goahttp.Muxer, w io.Writer) func(http.Handler) http.Handler {
 				buf.WriteByte('\n')
 				lines := strings.Split(string(b), "\n")
 				for _, line := range lines {
-					buf.WriteString(fmt.Sprintf("[%s] %s\n", reqID, line))
+					fmt.Fprintf(buf, "[%s] %s\n", reqID, line)
 				}
 			}
 			r.Body = io.NopCloser(bytes.NewBuffer(b))
@@ -81,7 +81,7 @@ func Debug(mux goahttp.Muxer, w io.Writer) func(http.Handler) http.Handler {
 			dupper := &responseDupper{ResponseWriter: rw, Buffer: &bytes.Buffer{}}
 			h.ServeHTTP(dupper, r)
 
-			buf.WriteString(fmt.Sprintf("\n< [%s] %s", reqID, http.StatusText(dupper.Status)))
+			fmt.Fprintf(buf, "\n< [%s] %s", reqID, http.StatusText(dupper.Status))
 			keys = make([]string, len(dupper.Header()))
 			i = 0
 			for k := range dupper.Header() {
@@ -90,13 +90,13 @@ func Debug(mux goahttp.Muxer, w io.Writer) func(http.Handler) http.Handler {
 			}
 			sort.Strings(keys)
 			for _, k := range keys {
-				buf.WriteString(fmt.Sprintf("\n< [%s] %s: %s", reqID, k, strings.Join(dupper.Header()[k], ", ")))
+				fmt.Fprintf(buf, "\n< [%s] %s: %s", reqID, k, strings.Join(dupper.Header()[k], ", "))
 			}
 			if dupper.Buffer.Len() > 0 {
 				buf.WriteByte('\n')
 				lines := strings.Split(dupper.Buffer.String(), "\n")
 				for _, line := range lines {
-					buf.WriteString(fmt.Sprintf("[%s] %s\n", reqID, line))
+					fmt.Fprintf(buf, "[%s] %s\n", reqID, line)
 				}
 			}
 			buf.WriteByte('\n')
diff --git a/http/mux.go b/http/mux.go
index 522400a2..1f52cf8c 100644
--- a/http/mux.go
+++ b/http/mux.go
@@ -190,7 +190,7 @@ func (m *mux) ensureContext(r *http.Request) *chi.Context {
 	if ctx.RoutePattern() != "" {
 		return ctx // already initialized
 	}
-	if !m.Router.Match(ctx, r.Method, r.URL.Path) {
+	if !m.Match(ctx, r.Method, r.URL.Path) {
 		return nil // route not handled by chi
 	}
 	return ctx
diff --git a/middleware/log.go b/middleware/log.go
index 56bc3c92..a06b752a 100644
--- a/middleware/log.go
+++ b/middleware/log.go
@@ -41,6 +41,6 @@ func (a *adapter) Log(keyvals ...any) error {
 		vals[i/2] = v
 		fm.WriteString(fmt.Sprintf(" %s=%%+v", k))
 	}
-	a.Logger.Printf(strings.TrimSpace(fm.String()), vals...)
+	a.Printf(strings.TrimSpace(fm.String()), vals...)
 	return nil
 }
diff --git a/middleware/xray/segment.go b/middleware/xray/segment.go
index 7f76cc1b..cb4789d2 100644
--- a/middleware/xray/segment.go
+++ b/middleware/xray/segment.go
@@ -191,7 +191,7 @@ func (s *Segment) RecordError(e error) {
 	//
 	// first check if the other flags have already been set in case these methods are being
 	// called directly instead of using xray.WrapClient(), etc.
-	if !(s.Fault || s.Throttle) {
+	if !s.Fault && !s.Throttle {
 		s.Error = true
 	}
 	if s.Cause == nil {
diff --git a/pkg/version.go b/pkg/version.go
index 027e0aae..b223b034 100644
--- a/pkg/version.go
+++ b/pkg/version.go
@@ -10,9 +10,9 @@ const (
 	// Major version number
 	Major = 3
 	// Minor version number
-	Minor = 20
+	Minor = 21
 	// Build number
-	Build = 1
+	Build = 0
 	// Suffix - set to empty string in release tag commits.
 	Suffix = ""
 )


Test patch:
diff --git a/codegen/service/convert_test.go b/codegen/service/convert_test.go
index 479d3c1c..07704047 100644
--- a/codegen/service/convert_test.go
+++ b/codegen/service/convert_test.go
@@ -308,7 +308,7 @@ func objRecursive() *expr.UserTypeExpr {
 		},
 		TypeName: "objRecursiveT",
 	}
-	obj := res.AttributeExpr.Type.(*expr.Object)
+	obj := res.Type.(*expr.Object)
 	*obj = append(*obj, &expr.NamedAttributeExpr{
 		Name:      "Rec",
 		Attribute: &expr.AttributeExpr{Type: res}})
diff --git a/codegen/service/service_test.go b/codegen/service/service_test.go
index dce4304d..6d0197e6 100644
--- a/codegen/service/service_test.go
+++ b/codegen/service/service_test.go
@@ -56,6 +56,8 @@ func TestService(t *testing.T) {
 		{"service-bidirectional-streaming-no-payload", testdata.BidirectionalStreamingNoPayloadMethodDSL, testdata.BidirectionalStreamingNoPayloadMethod},
 		{"service-bidirectional-streaming-result-with-views", testdata.BidirectionalStreamingResultWithViewsMethodDSL, testdata.BidirectionalStreamingResultWithViewsMethod},
 		{"service-bidirectional-streaming-result-with-explicit-view", testdata.BidirectionalStreamingResultWithExplicitViewMethodDSL, testdata.BidirectionalStreamingResultWithExplicitViewMethod},
+		{"service-multiple-api-key-security", testdata.MultipleAPIKeySecurityDSL, testdata.MultipleAPIKeySecurity},
+		{"service-mixed-and-multiple-api-key-security", testdata.MixedAndMultipleAPIKeySecurityDSL, testdata.MixedAndMultipleAPIKeySecurity},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
diff --git a/codegen/service/testdata/service_code.go b/codegen/service/testdata/service_code.go
index b2d92cdf..36bea5dd 100644
--- a/codegen/service/testdata/service_code.go
+++ b/codegen/service/testdata/service_code.go
@@ -3315,3 +3315,79 @@ type Foo struct {
 	IntField *int
 }
 `
+
+const MultipleAPIKeySecurity = `
+// Service is the MultipleAPIKeySecurity service interface.
+type Service interface {
+	// A implements A.
+	A(context.Context, *APayload) (err error)
+}
+
+// Auther defines the authorization functions to be implemented by the service.
+type Auther interface {
+	// APIKeyAuth implements the authorization logic for the APIKey security scheme.
+	APIKeyAuth(ctx context.Context, key string, schema *security.APIKeyScheme) (context.Context, error)
+}
+
+// APIName is the name of the API as defined in the design.
+const APIName = "test api"
+
+// APIVersion is the version of the API as defined in the design.
+const APIVersion = "0.0.1"
+
+// ServiceName is the name of the service as defined in the design. This is the
+// same value that is set in the endpoint request contexts under the ServiceKey
+// key.
+const ServiceName = "MultipleAPIKeySecurity"
+
+// MethodNames lists the service method names as defined in the design. These
+// are the same values that are set in the endpoint request contexts under the
+// MethodKey key.
+var MethodNames = [1]string{"A"}
+
+// APayload is the payload type of the MultipleAPIKeySecurity service A method.
+type APayload struct {
+	APIKey   string
+	TenantID string
+}
+`
+
+const MixedAndMultipleAPIKeySecurity = `
+// Service is the MixedAndMultipleAPIKeySecurity service interface.
+type Service interface {
+	// A implements A.
+	A(context.Context, *APayload) (err error)
+}
+
+// Auther defines the authorization functions to be implemented by the service.
+type Auther interface {
+	// JWTAuth implements the authorization logic for the JWT security scheme.
+	JWTAuth(ctx context.Context, token string, schema *security.JWTScheme) (context.Context, error)
+	// APIKeyAuth implements the authorization logic for the APIKey security scheme.
+	APIKeyAuth(ctx context.Context, key string, schema *security.APIKeyScheme) (context.Context, error)
+}
+
+// APIName is the name of the API as defined in the design.
+const APIName = "test api"
+
+// APIVersion is the version of the API as defined in the design.
+const APIVersion = "0.0.1"
+
+// ServiceName is the name of the service as defined in the design. This is the
+// same value that is set in the endpoint request contexts under the ServiceKey
+// key.
+const ServiceName = "MixedAndMultipleAPIKeySecurity"
+
+// MethodNames lists the service method names as defined in the design. These
+// are the same values that are set in the endpoint request contexts under the
+// MethodKey key.
+var MethodNames = [1]string{"A"}
+
+// APayload is the payload type of the MixedAndMultipleAPIKeySecurity service A
+// method.
+type APayload struct {
+	JWT      *string
+	APIKey   *string
+	TenantID *string
+}
+`
diff --git a/codegen/service/testdata/service_dsls.go b/codegen/service/testdata/service_dsls.go
index 76f7f917..b018d745 100644
--- a/codegen/service/testdata/service_dsls.go
+++ b/codegen/service/testdata/service_dsls.go
@@ -981,3 +981,55 @@ var EndpointWithClientInterceptorDSL = func() {
 		})
 	})
 }
+
+var MultipleAPIKeySecurityDSL = func() {
+	var APIKeyAuth = APIKeySecurity("api_key")
+	var TenantKeyAuth = APIKeySecurity("tenant")
+
+	Service("MultipleAPIKeySecurity", func() {
+		Security(APIKeyAuth, TenantKeyAuth)
+		Method("A", func() {
+			Payload(func() {
+				APIKey("api_key", "api_key", String)
+				APIKey("tenant", "tenant_id", String)
+				Required("api_key", "tenant_id")
+			})
+
+			HTTP(func() {
+				POST("/")
+				Header("api_key:X-API-Key")
+				Header("tenant_id:X-Tenant")
+			})
+		})
+	})
+}
+
+var MixedAndMultipleAPIKeySecurityDSL = func() {
+	var Signature = JWTSecurity("jwt", func() {
+		Scope("api:read", "Read access to API")
+	})
+	var APIKeyAuth = APIKeySecurity("api_key")
+	var TenantKeyAuth = APIKeySecurity("tenant")
+
+	Service("MixedAndMultipleAPIKeySecurity", func() {
+		Security(Signature, func() {
+			Scope("api:read")
+		})
+		Security(APIKeyAuth, TenantKeyAuth)
+
+		Method("A", func() {
+			Payload(func() {
+				Token("jwt", String)
+				APIKey("api_key", "api_key", String)
+				APIKey("tenant", "tenant_id", String)
+			})
+
+			HTTP(func() {
+				POST("/")
+				Header("api_key:X-API-Key")
+				Header("tenant_id:X-Tenant")
+				Header("jwt:Authorization")
+			})
+		})
+	})
+}
diff --git a/codegen/testing.go b/codegen/testing.go
index 5a4c0a26..21d30b5d 100644
--- a/codegen/testing.go
+++ b/codegen/testing.go
@@ -66,7 +66,7 @@ func sectionCodeWithPrefix(t *testing.T, section *SectionTemplate, prefix string
 func FormatTestCode(t *testing.T, code string) string {
 	t.Helper()
 	tmp := CreateTempFile(t, code)
-	defer os.Remove(tmp)
+	defer func() { _ = os.Remove(tmp) }()
 	require.NoError(t, finalizeGoSource(tmp))
 	content, err := os.ReadFile(tmp)
 	require.NoError(t, err)
@@ -81,7 +81,7 @@ func CreateTempFile(t *testing.T, content string) string {
 	require.NoError(t, err)
 	_, err = f.WriteString(content)
 	if err != nil {
-		os.Remove(f.Name())
+		require.NoError(t, os.Remove(f.Name()))
 		t.Fatal(err)
 	}
 	require.NoError(t, f.Close())
diff --git a/dsl/result_type_test.go b/dsl/result_type_test.go
index 61a941af..0a065cde 100644
--- a/dsl/result_type_test.go
+++ b/dsl/result_type_test.go
@@ -84,7 +84,7 @@ func TestView(t *testing.T) {
 						found = true
 						for _, attr := range c.expectedViewAttrs[view] {
 							found2 := false
-							for _, attr2 := range *v.AttributeExpr.Type.(*expr.Object) {
+							for _, attr2 := range *v.Type.(*expr.Object) {
 								if attr2.Name == attr {
 									found2 = true
 									break
diff --git a/eval/eval_test.go b/eval/eval_test.go
index d334fec1..25ffa2d8 100644
--- a/eval/eval_test.go
+++ b/eval/eval_test.go
@@ -1,11 +1,14 @@
 package eval_test
 
 import (
+	"errors"
 	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	. "goa.design/goa/v3/dsl"
+	"goa.design/goa/v3/eval"
 	"goa.design/goa/v3/expr"
 )
 
@@ -114,3 +117,118 @@ func TestTooManyArgError(t *testing.T) {
 		})
 	}
 }
+
+// mockExpr is a test implementation of the Expression interface
+type mockExpr struct{}
+
+func (m *mockExpr) EvalName() string {
+	return "MockExpression"
+}
+
+// TestValidationErrors tests the ValidationErrors type methods
+func TestValidationErrors(t *testing.T) {
+
+	t.Run("Error", func(t *testing.T) {
+		// Test Error() method
+		expr1 := &mockExpr{}
+		expr2 := &mockExpr{}
+
+		verr := &eval.ValidationErrors{
+			Errors:      []error{errors.New("error1"), errors.New("error2")},
+			Expressions: []eval.Expression{expr1, expr2},
+		}
+
+		errStr := verr.Error()
+		assert.Contains(t, errStr, "MockExpression: error1")
+		assert.Contains(t, errStr, "MockExpression: error2")
+		assert.Contains(t, errStr, "\n") // Should contain a newline between errors
+	})
+
+	t.Run("Merge", func(t *testing.T) {
+		// Test Merge() method
+		expr1 := &mockExpr{}
+		expr2 := &mockExpr{}
+		expr3 := &mockExpr{}
+
+		verr1 := &eval.ValidationErrors{
+			Errors:      []error{errors.New("error1")},
+			Expressions: []eval.Expression{expr1},
+		}
+
+		verr2 := &eval.ValidationErrors{
+			Errors:      []error{errors.New("error2"), errors.New("error3")},
+			Expressions: []eval.Expression{expr2, expr3},
+		}
+
+		// Test merging with nil
+		verrCopy := *verr1
+		verrCopy.Merge(nil)
+		assert.Equal(t, 1, len(verrCopy.Errors))
+		assert.Equal(t, 1, len(verrCopy.Expressions))
+
+		// Test merging with empty but non-nil ValidationErrors
+		verrCopy2 := *verr1
+		emptyVerr := &eval.ValidationErrors{}
+		verrCopy2.Merge(emptyVerr)
+		assert.Equal(t, 1, len(verrCopy2.Errors), "Merging with empty ValidationErrors should not change the error count")
+		assert.Equal(t, 1, len(verrCopy2.Expressions), "Merging with empty ValidationErrors should not change the expression count")
+
+		// Test normal merge
+		verr1.Merge(verr2)
+		assert.Equal(t, 3, len(verr1.Errors))
+		assert.Equal(t, 3, len(verr1.Expressions))
+		assert.Equal(t, expr1, verr1.Expressions[0])
+		assert.Equal(t, expr2, verr1.Expressions[1])
+		assert.Equal(t, expr3, verr1.Expressions[2])
+	})
+
+	t.Run("Add", func(t *testing.T) {
+		// Test Add() method
+		expr1 := &mockExpr{}
+
+		verr := &eval.ValidationErrors{}
+		verr.Add(expr1, "test error %s", "message")
+
+		require.Equal(t, 1, len(verr.Errors))
+		require.Equal(t, 1, len(verr.Expressions))
+		assert.Equal(t, "test error message", verr.Errors[0].Error())
+		assert.Equal(t, expr1, verr.Expressions[0])
+	})
+
+	t.Run("AddError_Simple", func(t *testing.T) {
+		// Test AddError() with a simple error
+		expr1 := &mockExpr{}
+		simpleErr := errors.New("simple error")
+
+		verr := &eval.ValidationErrors{}
+		verr.AddError(expr1, simpleErr)
+
+		require.Equal(t, 1, len(verr.Errors))
+		require.Equal(t, 1, len(verr.Expressions))
+		assert.Equal(t, simpleErr, verr.Errors[0])
+		assert.Equal(t, expr1, verr.Expressions[0])
+	})
+
+	t.Run("AddError_ValidationErrors", func(t *testing.T) {
+		// Test AddError() with another ValidationErrors
+		expr1 := &mockExpr{}
+		expr2 := &mockExpr{}
+		expr3 := &mockExpr{}
+
+		nestedVerr := &eval.ValidationErrors{
+			Errors:      []error{errors.New("nested error 1"), errors.New("nested error 2")},
+			Expressions: []eval.Expression{expr2, expr3},
+		}
+
+		verr := &eval.ValidationErrors{}
+		verr.AddError(expr1, nestedVerr) // expr1 should be ignored since we're adding a ValidationErrors
+
+		// The nested ValidationErrors should be flattened
+		require.Equal(t, 2, len(verr.Errors))
+		require.Equal(t, 2, len(verr.Expressions))
+		assert.Equal(t, "nested error 1", verr.Errors[0].Error())
+		assert.Equal(t, "nested error 2", verr.Errors[1].Error())
+		assert.Equal(t, expr2, verr.Expressions[0])
+		assert.Equal(t, expr3, verr.Expressions[1])
+	})
+}
diff --git a/expr/http_endpoint_test.go b/expr/http_endpoint_test.go
index b1d40f45..751f47c5 100644
--- a/expr/http_endpoint_test.go
+++ b/expr/http_endpoint_test.go
@@ -72,7 +72,7 @@ func TestHTTPEndpointPrepare(t *testing.T) {
 				root := expr.RunDSL(t, c.DSL)
 				e := root.API.HTTP.Services[len(root.API.HTTP.Services)-1].HTTPEndpoints[0]
 
-				ht := expr.AsObject(e.Headers.AttributeExpr.Type)
+				ht := expr.AsObject(e.Headers.Type)
 				if len(*ht) != len(c.Headers) {
 					t.Errorf("got %d headers, expected %d", len(*ht), len(c.Headers))
 				} else {
@@ -83,7 +83,7 @@ func TestHTTPEndpointPrepare(t *testing.T) {
 					}
 				}
 
-				ct := expr.AsObject(e.Cookies.AttributeExpr.Type)
+				ct := expr.AsObject(e.Cookies.Type)
 				if len(*ct) != len(c.Cookies) {
 					t.Errorf("got %d cookies, expected %d", len(*ct), len(c.Cookies))
 				} else {
@@ -94,7 +94,7 @@ func TestHTTPEndpointPrepare(t *testing.T) {
 					}
 				}
 
-				pt := expr.AsObject(e.Params.AttributeExpr.Type)
+				pt := expr.AsObject(e.Params.Type)
 				if len(*pt) != len(c.Params) {
 					t.Errorf("got %d params, expected %d", len(*pt), len(c.Params))
 				} else {
diff --git a/expr/http_sse_test.go b/expr/http_sse_test.go
new file mode 100644
index 00000000..da731f2f
--- /dev/null
+++ b/expr/http_sse_test.go
@@ -0,0 +1,175 @@
+package expr_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"goa.design/goa/v3/expr"
+)
+
+func TestHTTPSSEExprValidation(t *testing.T) {
+	cases := map[string]struct {
+		SSE          *expr.HTTPSSEExpr
+		Payload      *expr.AttributeExpr
+		Result       *expr.AttributeExpr
+		ExpectedErrs []string
+	}{
+		"valid-empty": {
+			SSE:     &expr.HTTPSSEExpr{},
+			Payload: &expr.AttributeExpr{Type: &expr.Object{}},
+			Result:  &expr.AttributeExpr{Type: &expr.Object{}},
+		},
+		"valid-with-fields": {
+			SSE: &expr.HTTPSSEExpr{
+				RequestIDField: "request_id",
+				DataField:      "data",
+				IDField:        "id",
+				EventField:     "event",
+				RetryField:     "retry",
+			},
+			Payload: &expr.AttributeExpr{
+				Type: &expr.Object{
+					&expr.NamedAttributeExpr{Name: "request_id", Attribute: &expr.AttributeExpr{Type: expr.String}},
+				},
+			},
+			Result: &expr.AttributeExpr{
+				Type: &expr.Object{
+					&expr.NamedAttributeExpr{Name: "data", Attribute: &expr.AttributeExpr{Type: expr.String}},
+					&expr.NamedAttributeExpr{Name: "id", Attribute: &expr.AttributeExpr{Type: expr.String}},
+					&expr.NamedAttributeExpr{Name: "event", Attribute: &expr.AttributeExpr{Type: expr.String}},
+					&expr.NamedAttributeExpr{Name: "retry", Attribute: &expr.AttributeExpr{Type: expr.Int}},
+				},
+			},
+		},
+		"invalid-id-field-type": {
+			SSE: &expr.HTTPSSEExpr{
+				IDField: "id",
+			},
+			Payload: &expr.AttributeExpr{Type: &expr.Object{}},
+			Result: &expr.AttributeExpr{
+				Type: &expr.Object{
+					&expr.NamedAttributeExpr{Name: "id", Attribute: &expr.AttributeExpr{Type: expr.Int}},
+				},
+			},
+			ExpectedErrs: []string{"cannot use \"id\" for SSE event id field: attribute type must be one of string"},
+		},
+		"invalid-request-id-field-type": {
+			SSE: &expr.HTTPSSEExpr{
+				RequestIDField: "request_id",
+			},
+			Payload: &expr.AttributeExpr{
+				Type: &expr.Object{
+					&expr.NamedAttributeExpr{Name: "request_id", Attribute: &expr.AttributeExpr{Type: expr.Int}},
+				},
+			},
+			Result:       &expr.AttributeExpr{Type: &expr.Object{}},
+			ExpectedErrs: []string{"cannot use \"request_id\" for SSE request ID field: attribute type must be one of string"},
+		},
+		"invalid-event-field-type": {
+			SSE: &expr.HTTPSSEExpr{
+				EventField: "event",
+			},
+			Payload: &expr.AttributeExpr{Type: &expr.Object{}},
+			Result: &expr.AttributeExpr{
+				Type: &expr.Object{
+					&expr.NamedAttributeExpr{Name: "event", Attribute: &expr.AttributeExpr{Type: expr.Int}},
+				},
+			},
+			ExpectedErrs: []string{"cannot use \"event\" for SSE event type field: attribute type must be one of string"},
+		},
+		"invalid-retry-field-type": {
+			SSE: &expr.HTTPSSEExpr{
+				RetryField: "retry",
+			},
+			Payload: &expr.AttributeExpr{Type: &expr.Object{}},
+			Result: &expr.AttributeExpr{
+				Type: &expr.Object{
+					&expr.NamedAttributeExpr{Name: "retry", Attribute: &expr.AttributeExpr{Type: expr.Boolean}},
+				},
+			},
+			ExpectedErrs: []string{"cannot use \"retry\" for SSE event retry field: attribute type must be one of int, int32, int64, uint, uint32, uint64"},
+		},
+		"missing-field": {
+			SSE: &expr.HTTPSSEExpr{
+				DataField: "missing",
+			},
+			Payload: &expr.AttributeExpr{Type: &expr.Object{}},
+			Result: &expr.AttributeExpr{
+				Type: &expr.Object{
+					&expr.NamedAttributeExpr{Name: "data", Attribute: &expr.AttributeExpr{Type: expr.String}},
+				},
+			},
+			ExpectedErrs: []string{"cannot use \"missing\" for SSE event data field: attribute not found in result type"},
+		},
+		"missing-request-id-field": {
+			SSE: &expr.HTTPSSEExpr{
+				RequestIDField: "missing",
+			},
+			Payload: &expr.AttributeExpr{
+				Type: &expr.Object{
+					&expr.NamedAttributeExpr{Name: "request_id", Attribute: &expr.AttributeExpr{Type: expr.String}},
+				},
+			},
+			Result:       &expr.AttributeExpr{Type: &expr.Object{}},
+			ExpectedErrs: []string{"cannot use \"missing\" for SSE request ID field: attribute not found in result type"},
+		},
+		"nil-result-type": {
+			SSE: &expr.HTTPSSEExpr{
+				DataField: "data",
+			},
+			Payload: &expr.AttributeExpr{Type: &expr.Object{}},
+		},
+		"nil-payload-type": {
+			SSE: &expr.HTTPSSEExpr{
+				RequestIDField: "request_id",
+			},
+		},
+		"empty-result-type": {
+			SSE: &expr.HTTPSSEExpr{
+				DataField: "data",
+			},
+			Payload:      &expr.AttributeExpr{Type: &expr.Object{}},
+			Result:       &expr.AttributeExpr{Type: expr.Int},
+			ExpectedErrs: []string{"cannot use \"data\" for SSE event data field: result type is not an object"},
+		},
+		"non-object-result-type": {
+			SSE: &expr.HTTPSSEExpr{
+				DataField: "data",
+			},
+			Payload:      &expr.AttributeExpr{Type: &expr.Object{}},
+			Result:       &expr.AttributeExpr{Type: expr.String},
+			ExpectedErrs: []string{"cannot use \"data\" for SSE event data field: result type is not an object"},
+		},
+	}
+
+	for name, tc := range cases {
+		t.Run(name, func(t *testing.T) {
+			// Create a mock HTTP endpoint expression for validation
+			methodExpr := &expr.MethodExpr{
+				Name:    "TestMethod",
+				Payload: tc.Payload,
+				Result:  tc.Result,
+				Stream:  expr.ServerStreamKind, // Must be a streaming method for SSE
+			}
+			endpoint := &expr.HTTPEndpointExpr{
+				MethodExpr: methodExpr,
+				SSE:        tc.SSE,
+			}
+
+			// Run validation
+			err := tc.SSE.Validate(endpoint)
+
+			// Check results
+			if len(tc.ExpectedErrs) == 0 {
+				require.NoError(t, err, "expected no error")
+			} else {
+				require.Error(t, err, "expected error, got none")
+				for _, expected := range tc.ExpectedErrs {
+					assert.Contains(t, err.Error(), expected, "error should contain expected message")
+				}
+			}
+		})
+	}
+}
diff --git a/expr/project_test.go b/expr/project_test.go
index 9068f82f..7b5b11ae 100644
--- a/expr/project_test.go
+++ b/expr/project_test.go
@@ -59,13 +59,13 @@ func TestProject(t *testing.T) {
 				t.Fatal(err)
 			}
 			if !Equal(projected, k.Expected) {
-				projected.AttributeExpr.Debug("got")
-				k.Expected.AttributeExpr.Debug("expected")
+				projected.Debug("got")
+				k.Expected.Debug("expected")
 				t.Errorf("got: %s, expected: %s\n", Hash(projected, false, true, true), Hash(k.Expected, false, true, true))
 			}
-			if pobj := AsObject(projected.AttributeExpr.Type); pobj != nil {
+			if pobj := AsObject(projected.Type); pobj != nil {
 				for _, att := range *pobj {
-					att2 := k.Expected.AttributeExpr.Find(att.Name)
+					att2 := k.Expected.Find(att.Name)
 					if att2 == nil {
 						continue
 					}
diff --git a/expr/testing.go b/expr/testing.go
index fce810a1..610ef169 100644
--- a/expr/testing.go
+++ b/expr/testing.go
@@ -57,7 +57,7 @@ func CreateTempFile(t *testing.T, content string) string {
 	}
 	_, err = f.WriteString(content)
 	if err != nil {
-		os.Remove(f.Name())
+		_ = os.Remove(f.Name())
 		t.Fatal(err)
 	}
 	if err := f.Close(); err != nil {
diff --git a/http/codegen/example_cli_test.go b/http/codegen/example_cli_test.go
index c6de2bae..6b12fea1 100644
--- a/http/codegen/example_cli_test.go
+++ b/http/codegen/example_cli_test.go
@@ -38,7 +38,7 @@ func TestExampleCLIFiles(t *testing.T) {
 				require.NoError(t, s.Write(&buf))
 			}
 			code := codegen.FormatTestCode(t, "package foo\n"+buf.String())
-			golden := filepath.Join("testdata", "client-"+c.Name+".golden")
+			golden := filepath.Join("testdata", "golden", "client-"+c.Name+".golden")
 			compareOrUpdateGolden(t, code, golden)
 		})
 	}
diff --git a/http/codegen/example_server_test.go b/http/codegen/example_server_test.go
index ffd6e1e7..e0f61014 100644
--- a/http/codegen/example_server_test.go
+++ b/http/codegen/example_server_test.go
@@ -106,7 +106,7 @@ func TestExampleServerFiles(t *testing.T) {
 					require.NoError(t, s.Write(&buf))
 				}
 				code := codegen.FormatTestCode(t, "package foo\n"+buf.String())
-				golden := filepath.Join("testdata", "server-"+c.Name+".golden")
+				golden := filepath.Join("testdata", "golden", "server-"+c.Name+".golden")
 				compareOrUpdateGolden(t, code, golden)
 			})
 		}
diff --git a/http/codegen/openapi/v3/types_test.go b/http/codegen/openapi/v3/types_test.go
index aa3b0136..574bf3a1 100644
--- a/http/codegen/openapi/v3/types_test.go
+++ b/http/codegen/openapi/v3/types_test.go
@@ -472,7 +472,7 @@ func newRT(id string, att *expr.AttributeExpr) *expr.AttributeExpr {
 // Helper function for result types with views
 func newRTWithView(id string, att *expr.AttributeExpr, view string) *expr.AttributeExpr {
 	rt := newRT(id, att)
-	rt.Type.(*expr.ResultTypeExpr).AttributeExpr.Meta = expr.MetaExpr{
+	rt.Type.(*expr.ResultTypeExpr).Meta = expr.MetaExpr{
 		expr.ViewMetaKey: []string{view},
 	}
 	return rt
diff --git a/http/codegen/server_encode_test.go b/http/codegen/server_encode_test.go
index c9c3c2d9..80d8fb0b 100644
--- a/http/codegen/server_encode_test.go
+++ b/http/codegen/server_encode_test.go
@@ -85,6 +85,8 @@ func TestEncode(t *testing.T) {
 		{"empty-server-response", testdata.EmptyServerResponseDSL, testdata.EmptyServerResponseEncodeCode},
 		{"empty-server-response-with-tags", testdata.EmptyServerResponseWithTagsDSL, testdata.EmptyServerResponseWithTagsEncodeCode},
 
+		{"skip-response-body-encode-decode", testdata.ResponseEncoderSkipResponseBodyEncodeDecodeDSL, testdata.ResponseEncoderSkipResponseBodyEncodeDecodeCode},
+
 		{"result-with-custom-pkg-type", testdata.ResultWithCustomPkgTypeDSL, testdata.ResultWithCustomPkgTypeEncodeCode},
 		{"result-with-embedded-custom-pkg-type", testdata.EmbeddedCustomPkgTypeDSL, testdata.ResultWithEmbeddedCustomPkgTypeEncodeCode},
 	}
diff --git a/http/codegen/sse_server_test.go b/http/codegen/sse_server_test.go
new file mode 100644
index 00000000..118a3b07
--- /dev/null
+++ b/http/codegen/sse_server_test.go
@@ -0,0 +1,41 @@
+package codegen
+
+import (
+	"path/filepath"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+
+	"goa.design/goa/v3/codegen"
+	"goa.design/goa/v3/expr"
+	"goa.design/goa/v3/http/codegen/testdata"
+)
+
+func TestSSE(t *testing.T) {
+	cases := []struct {
+		Name string
+		DSL  func()
+	}{
+		{"string", testdata.SSEStringDSL},
+		{"int", testdata.SSEIntDSL},
+		{"bool", testdata.SSEBoolDSL},
+		{"object", testdata.SSEObjectDSL},
+		{"data-field", testdata.SSEDataFieldDSL},
+		{"data-id-field", testdata.SSEDataIDFieldDSL},
+		{"request-id", testdata.SSERequestIDDSL},
+		{"all-fields", testdata.SSEAllFieldsDSL},
+	}
+
+	for _, c := range cases {
+		t.Run(c.Name, func(t *testing.T) {
+			RunHTTPDSL(t, c.DSL)
+			fs := ServerFiles("", expr.Root)
+			require.Len(t, fs, 3)
+			sections := fs[1].SectionTemplates
+			require.Greater(t, len(sections), 1)
+			code := codegen.SectionCode(t, sections[1])
+			golden := filepath.Join("testdata", "golden", "sse-"+c.Name+".golden")
+			compareOrUpdateGolden(t, code, golden)
+		})
+	}
+}
diff --git a/http/codegen/testdata/client-no-server.golden b/http/codegen/testdata/golden/client-no-server.golden
similarity index 100%
rename from http/codegen/testdata/client-no-server.golden
rename to http/codegen/testdata/golden/client-no-server.golden
diff --git a/http/codegen/testdata/client-server-hosting-multiple-services.golden b/http/codegen/testdata/golden/client-server-hosting-multiple-services.golden
similarity index 100%
rename from http/codegen/testdata/client-server-hosting-multiple-services.golden
rename to http/codegen/testdata/golden/client-server-hosting-multiple-services.golden
diff --git a/http/codegen/testdata/client-server-hosting-service-subset.golden b/http/codegen/testdata/golden/client-server-hosting-service-subset.golden
similarity index 100%
rename from http/codegen/testdata/client-server-hosting-service-subset.golden
rename to http/codegen/testdata/golden/client-server-hosting-service-subset.golden
diff --git a/http/codegen/testdata/client-streaming-multiple-services.golden b/http/codegen/testdata/golden/client-streaming-multiple-services.golden
similarity index 100%
rename from http/codegen/testdata/client-streaming-multiple-services.golden
rename to http/codegen/testdata/golden/client-streaming-multiple-services.golden
diff --git a/http/codegen/testdata/client-streaming.golden b/http/codegen/testdata/golden/client-streaming.golden
similarity index 100%
rename from http/codegen/testdata/client-streaming.golden
rename to http/codegen/testdata/golden/client-streaming.golden
diff --git a/http/codegen/testdata/server-no-server.golden b/http/codegen/testdata/golden/server-no-server.golden
similarity index 100%
rename from http/codegen/testdata/server-no-server.golden
rename to http/codegen/testdata/golden/server-no-server.golden
diff --git a/http/codegen/testdata/server-server-hosting-multiple-services.golden b/http/codegen/testdata/golden/server-server-hosting-multiple-services.golden
similarity index 100%
rename from http/codegen/testdata/server-server-hosting-multiple-services.golden
rename to http/codegen/testdata/golden/server-server-hosting-multiple-services.golden
diff --git a/http/codegen/testdata/server-server-hosting-service-subset.golden b/http/codegen/testdata/golden/server-server-hosting-service-subset.golden
similarity index 100%
rename from http/codegen/testdata/server-server-hosting-service-subset.golden
rename to http/codegen/testdata/golden/server-server-hosting-service-subset.golden
diff --git a/http/codegen/testdata/server-server-hosting-service-with-file-server.golden b/http/codegen/testdata/golden/server-server-hosting-service-with-file-server.golden
similarity index 100%
rename from http/codegen/testdata/server-server-hosting-service-with-file-server.golden
rename to http/codegen/testdata/golden/server-server-hosting-service-with-file-server.golden
diff --git a/http/codegen/testdata/server-streaming.golden b/http/codegen/testdata/golden/server-streaming.golden
similarity index 100%
rename from http/codegen/testdata/server-streaming.golden
rename to http/codegen/testdata/golden/server-streaming.golden
diff --git a/http/codegen/testdata/golden/sse-all-fields.golden b/http/codegen/testdata/golden/sse-all-fields.golden
new file mode 100644
index 00000000..6d0df711
--- /dev/null
+++ b/http/codegen/testdata/golden/sse-all-fields.golden
@@ -0,0 +1,68 @@
+// SSEAllFieldsMethodServerStream implements the
+// sseallfieldsservice.SSEAllFieldsMethodServerStream interface using
+// Server-Sent Events.
+type SSEAllFieldsMethodServerStream struct {
+	// once ensures the headers are written once.
+	once sync.Once
+	// w is the HTTP response writer used to send the SSE events.
+	w http.ResponseWriter
+	// r is the HTTP request.
+	r *http.Request
+}
+
+// Send Send streams instances of
+// "sseallfieldsservice.SSEAllFieldsMethodResult" to the "SSEAllFieldsMethod"
+// endpoint SSE connection.
+func (s *SSEAllFieldsMethodServerStream) Send(v *sseallfieldsservice.SSEAllFieldsMethodResult) error {
+	return s.SendWithContext(context.Background(), v)
+}
+
+// SendWithContext SendWithContext streams instances of
+// "sseallfieldsservice.SSEAllFieldsMethodResult" to the "SSEAllFieldsMethod"
+// endpoint SSE connection with context.
+func (s *SSEAllFieldsMethodServerStream) SendWithContext(ctx context.Context, v *sseallfieldsservice.SSEAllFieldsMethodResult) error {
+	s.once.Do(func() {
+		header := s.w.Header()
+		if header.Get("Content-Type") == "" {
+			header.Set("Content-Type", "text/event-stream")
+		}
+		if header.Get("Cache-Control") == "" {
+			header.Set("Cache-Control", "no-cache")
+		}
+		if header.Get("Connection") == "" {
+			header.Set("Connection", "keep-alive")
+		}
+		s.w.WriteHeader(http.StatusOK)
+	})
+	res := v
+
+	if id := res.id; id != "" {
+		fmt.Fprintf(s.w, "id: %s\n", id)
+	}
+	if event := res.event; event != "" {
+		fmt.Fprintf(s.w, "event: %s\n", event)
+	}
+	if retry := res.retry; retry > 0 {
+		fmt.Fprintf(s.w, "retry: %d\n", retry)
+	}
+
+	var data string
+	dataField := res.data
+	byts, err := json.Marshal(dataField)
+	if err != nil {
+		return err
+	}
+	data = string(byts)
+	fmt.Fprintf(s.w, "data: %s\n\n", data)
+
+	if f, ok := s.w.(http.Flusher); ok {
+		f.Flush()
+	}
+	return nil
+}
+
+// Close is a no-op for SSE. We keep the method for compatibility with other
+// stream types.
+func (s *SSEAllFieldsMethodServerStream) Close() error {
+	return nil
+}
diff --git a/http/codegen/testdata/golden/sse-bool.golden b/http/codegen/testdata/golden/sse-bool.golden
new file mode 100644
index 00000000..b58d798c
--- /dev/null
+++ b/http/codegen/testdata/golden/sse-bool.golden
@@ -0,0 +1,54 @@
+// SSEBoolMethodServerStream implements the
+// sseboolservice.SSEBoolMethodServerStream interface using Server-Sent Events.
+type SSEBoolMethodServerStream struct {
+	// once ensures the headers are written once.
+	once sync.Once
+	// w is the HTTP response writer used to send the SSE events.
+	w http.ResponseWriter
+	// r is the HTTP request.
+	r *http.Request
+}
+
+// Send Send streams instances of "bool" to the "SSEBoolMethod" endpoint SSE
+// connection.
+func (s *SSEBoolMethodServerStream) Send(v bool) error {
+	return s.SendWithContext(context.Background(), v)
+}
+
+// SendWithContext SendWithContext streams instances of "bool" to the
+// "SSEBoolMethod" endpoint SSE connection with context.
+func (s *SSEBoolMethodServerStream) SendWithContext(ctx context.Context, v bool) error {
+	s.once.Do(func() {
+		header := s.w.Header()
+		if header.Get("Content-Type") == "" {
+			header.Set("Content-Type", "text/event-stream")
+		}
+		if header.Get("Cache-Control") == "" {
+			header.Set("Cache-Control", "no-cache")
+		}
+		if header.Get("Connection") == "" {
+			header.Set("Connection", "keep-alive")
+		}
+		s.w.WriteHeader(http.StatusOK)
+	})
+	res := v
+
+	var data string
+	byts, err := json.Marshal(res)
+	if err != nil {
+		return err
+	}
+	data = string(byts)
+	fmt.Fprintf(s.w, "data: %s\n\n", data)
+
+	if f, ok := s.w.(http.Flusher); ok {
+		f.Flush()
+	}
+	return nil
+}
+
+// Close is a no-op for SSE. We keep the method for compatibility with other
+// stream types.
+func (s *SSEBoolMethodServerStream) Close() error {
+	return nil
+}
diff --git a/http/codegen/testdata/golden/sse-data-field.golden b/http/codegen/testdata/golden/sse-data-field.golden
new file mode 100644
index 00000000..a09ace1c
--- /dev/null
+++ b/http/codegen/testdata/golden/sse-data-field.golden
@@ -0,0 +1,58 @@
+// SSEDataFieldMethodServerStream implements the
+// ssedatafieldservice.SSEDataFieldMethodServerStream interface using
+// Server-Sent Events.
+type SSEDataFieldMethodServerStream struct {
+	// once ensures the headers are written once.
+	once sync.Once
+	// w is the HTTP response writer used to send the SSE events.
+	w http.ResponseWriter
+	// r is the HTTP request.
+	r *http.Request
+}
+
+// Send Send streams instances of
+// "ssedatafieldservice.SSEDataFieldMethodResult" to the "SSEDataFieldMethod"
+// endpoint SSE connection.
+func (s *SSEDataFieldMethodServerStream) Send(v *ssedatafieldservice.SSEDataFieldMethodResult) error {
+	return s.SendWithContext(context.Background(), v)
+}
+
+// SendWithContext SendWithContext streams instances of
+// "ssedatafieldservice.SSEDataFieldMethodResult" to the "SSEDataFieldMethod"
+// endpoint SSE connection with context.
+func (s *SSEDataFieldMethodServerStream) SendWithContext(ctx context.Context, v *ssedatafieldservice.SSEDataFieldMethodResult) error {
+	s.once.Do(func() {
+		header := s.w.Header()
+		if header.Get("Content-Type") == "" {
+			header.Set("Content-Type", "text/event-stream")
+		}
+		if header.Get("Cache-Control") == "" {
+			header.Set("Cache-Control", "no-cache")
+		}
+		if header.Get("Connection") == "" {
+			header.Set("Connection", "keep-alive")
+		}
+		s.w.WriteHeader(http.StatusOK)
+	})
+	res := v
+
+	var data string
+	dataField := res.data
+	byts, err := json.Marshal(dataField)
+	if err != nil {
+		return err
+	}
+	data = string(byts)
+	fmt.Fprintf(s.w, "data: %s\n\n", data)
+
+	if f, ok := s.w.(http.Flusher); ok {
+		f.Flush()
+	}
+	return nil
+}
+
+// Close is a no-op for SSE. We keep the method for compatibility with other
+// stream types.
+func (s *SSEDataFieldMethodServerStream) Close() error {
+	return nil
+}
diff --git a/http/codegen/testdata/golden/sse-data-id-field.golden b/http/codegen/testdata/golden/sse-data-id-field.golden
new file mode 100644
index 00000000..a1ee61b4
--- /dev/null
+++ b/http/codegen/testdata/golden/sse-data-id-field.golden
@@ -0,0 +1,62 @@
+// SSEDataIDFieldMethodServerStream implements the
+// ssedataidfieldservice.SSEDataIDFieldMethodServerStream interface using
+// Server-Sent Events.
+type SSEDataIDFieldMethodServerStream struct {
+	// once ensures the headers are written once.
+	once sync.Once
+	// w is the HTTP response writer used to send the SSE events.
+	w http.ResponseWriter
+	// r is the HTTP request.
+	r *http.Request
+}
+
+// Send Send streams instances of
+// "ssedataidfieldservice.SSEDataIDFieldMethodResult" to the
+// "SSEDataIDFieldMethod" endpoint SSE connection.
+func (s *SSEDataIDFieldMethodServerStream) Send(v *ssedataidfieldservice.SSEDataIDFieldMethodResult) error {
+	return s.SendWithContext(context.Background(), v)
+}
+
+// SendWithContext SendWithContext streams instances of
+// "ssedataidfieldservice.SSEDataIDFieldMethodResult" to the
+// "SSEDataIDFieldMethod" endpoint SSE connection with context.
+func (s *SSEDataIDFieldMethodServerStream) SendWithContext(ctx context.Context, v *ssedataidfieldservice.SSEDataIDFieldMethodResult) error {
+	s.once.Do(func() {
+		header := s.w.Header()
+		if header.Get("Content-Type") == "" {
+			header.Set("Content-Type", "text/event-stream")
+		}
+		if header.Get("Cache-Control") == "" {
+			header.Set("Cache-Control", "no-cache")
+		}
+		if header.Get("Connection") == "" {
+			header.Set("Connection", "keep-alive")
+		}
+		s.w.WriteHeader(http.StatusOK)
+	})
+	res := v
+
+	if id := res.id; id != "" {
+		fmt.Fprintf(s.w, "id: %s\n", id)
+	}
+
+	var data string
+	dataField := res.data
+	byts, err := json.Marshal(dataField)
+	if err != nil {
+		return err
+	}
+	data = string(byts)
+	fmt.Fprintf(s.w, "data: %s\n\n", data)
+
+	if f, ok := s.w.(http.Flusher); ok {
+		f.Flush()
+	}
+	return nil
+}
+
+// Close is a no-op for SSE. We keep the method for compatibility with other
+// stream types.
+func (s *SSEDataIDFieldMethodServerStream) Close() error {
+	return nil
+}
diff --git a/http/codegen/testdata/golden/sse-int.golden b/http/codegen/testdata/golden/sse-int.golden
new file mode 100644
index 00000000..d789c179
--- /dev/null
+++ b/http/codegen/testdata/golden/sse-int.golden
@@ -0,0 +1,50 @@
+// SSEIntMethodServerStream implements the
+// sseintservice.SSEIntMethodServerStream interface using Server-Sent Events.
+type SSEIntMethodServerStream struct {
+	// once ensures the headers are written once.
+	once sync.Once
+	// w is the HTTP response writer used to send the SSE events.
+	w http.ResponseWriter
+	// r is the HTTP request.
+	r *http.Request
+}
+
+// Send Send streams instances of "int" to the "SSEIntMethod" endpoint SSE
+// connection.
+func (s *SSEIntMethodServerStream) Send(v int) error {
+	return s.SendWithContext(context.Background(), v)
+}
+
+// SendWithContext SendWithContext streams instances of "int" to the
+// "SSEIntMethod" endpoint SSE connection with context.
+func (s *SSEIntMethodServerStream) SendWithContext(ctx context.Context, v int) error {
+	s.once.Do(func() {
+		header := s.w.Header()
+		if header.Get("Content-Type") == "" {
+			header.Set("Content-Type", "text/event-stream")
+		}
+		if header.Get("Cache-Control") == "" {
+			header.Set("Cache-Control", "no-cache")
+		}
+		if header.Get("Connection") == "" {
+			header.Set("Connection", "keep-alive")
+		}
+		s.w.WriteHeader(http.StatusOK)
+	})
+	res := v
+
+	var data string
+	data = fmt.Sprintf("%d", res)
+	fmt.Fprintf(s.w, "data: %s\n\n", data)
+
+	if f, ok := s.w.(http.Flusher); ok {
+		f.Flush()
+	}
+	return nil
+}
+
+// Close is a no-op for SSE. We keep the method for compatibility with other
+// stream types.
+func (s *SSEIntMethodServerStream) Close() error {
+	return nil
+}
diff --git a/http/codegen/testdata/golden/sse-object.golden b/http/codegen/testdata/golden/sse-object.golden
new file mode 100644
index 00000000..a5f03b2c
--- /dev/null
+++ b/http/codegen/testdata/golden/sse-object.golden
@@ -0,0 +1,56 @@
+// SSEObjectMethodServerStream implements the
+// sseobjectservice.SSEObjectMethodServerStream interface using Server-Sent
+// Events.
+type SSEObjectMethodServerStream struct {
+	// once ensures the headers are written once.
+	once sync.Once
+	// w is the HTTP response writer used to send the SSE events.
+	w http.ResponseWriter
+	// r is the HTTP request.
+	r *http.Request
+}
+
+// Send Send streams instances of "sseobjectservice.SSEObjectMethodResult" to
+// the "SSEObjectMethod" endpoint SSE connection.
+func (s *SSEObjectMethodServerStream) Send(v *sseobjectservice.SSEObjectMethodResult) error {
+	return s.SendWithContext(context.Background(), v)
+}
+
+// SendWithContext SendWithContext streams instances of
+// "sseobjectservice.SSEObjectMethodResult" to the "SSEObjectMethod" endpoint
+// SSE connection with context.
+func (s *SSEObjectMethodServerStream) SendWithContext(ctx context.Context, v *sseobjectservice.SSEObjectMethodResult) error {
+	s.once.Do(func() {
+		header := s.w.Header()
+		if header.Get("Content-Type") == "" {
+			header.Set("Content-Type", "text/event-stream")
+		}
+		if header.Get("Cache-Control") == "" {
+			header.Set("Cache-Control", "no-cache")
+		}
+		if header.Get("Connection") == "" {
+			header.Set("Connection", "keep-alive")
+		}
+		s.w.WriteHeader(http.StatusOK)
+	})
+	res := v
+
+	var data string
+	byts, err := json.Marshal(res)
+	if err != nil {
+		return err
+	}
+	data = string(byts)
+	fmt.Fprintf(s.w, "data: %s\n\n", data)
+
+	if f, ok := s.w.(http.Flusher); ok {
+		f.Flush()
+	}
+	return nil
+}
+
+// Close is a no-op for SSE. We keep the method for compatibility with other
+// stream types.
+func (s *SSEObjectMethodServerStream) Close() error {
+	return nil
+}
diff --git a/http/codegen/testdata/golden/sse-request-id.golden b/http/codegen/testdata/golden/sse-request-id.golden
new file mode 100644
index 00000000..96890d35
--- /dev/null
+++ b/http/codegen/testdata/golden/sse-request-id.golden
@@ -0,0 +1,51 @@
+// SSERequestIDMethodServerStream implements the
+// sserequestidservice.SSERequestIDMethodServerStream interface using
+// Server-Sent Events.
+type SSERequestIDMethodServerStream struct {
+	// once ensures the headers are written once.
+	once sync.Once
+	// w is the HTTP response writer used to send the SSE events.
+	w http.ResponseWriter
+	// r is the HTTP request.
+	r *http.Request
+}
+
+// Send Send streams instances of "string" to the "SSERequestIDMethod" endpoint
+// SSE connection.
+func (s *SSERequestIDMethodServerStream) Send(v string) error {
+	return s.SendWithContext(context.Background(), v)
+}
+
+// SendWithContext SendWithContext streams instances of "string" to the
+// "SSERequestIDMethod" endpoint SSE connection with context.
+func (s *SSERequestIDMethodServerStream) SendWithContext(ctx context.Context, v string) error {
+	s.once.Do(func() {
+		header := s.w.Header()
+		if header.Get("Content-Type") == "" {
+			header.Set("Content-Type", "text/event-stream")
+		}
+		if header.Get("Cache-Control") == "" {
+			header.Set("Cache-Control", "no-cache")
+		}
+		if header.Get("Connection") == "" {
+			header.Set("Connection", "keep-alive")
+		}
+		s.w.WriteHeader(http.StatusOK)
+	})
+	res := v
+
+	var data string
+	data = res
+	fmt.Fprintf(s.w, "data: %s\n\n", data)
+
+	if f, ok := s.w.(http.Flusher); ok {
+		f.Flush()
+	}
+	return nil
+}
+
+// Close is a no-op for SSE. We keep the method for compatibility with other
+// stream types.
+func (s *SSERequestIDMethodServerStream) Close() error {
+	return nil
+}
diff --git a/http/codegen/testdata/golden/sse-string.golden b/http/codegen/testdata/golden/sse-string.golden
new file mode 100644
index 00000000..e013da59
--- /dev/null
+++ b/http/codegen/testdata/golden/sse-string.golden
@@ -0,0 +1,51 @@
+// SSEStringMethodServerStream implements the
+// ssestringservice.SSEStringMethodServerStream interface using Server-Sent
+// Events.
+type SSEStringMethodServerStream struct {
+	// once ensures the headers are written once.
+	once sync.Once
+	// w is the HTTP response writer used to send the SSE events.
+	w http.ResponseWriter
+	// r is the HTTP request.
+	r *http.Request
+}
+
+// Send Send streams instances of "string" to the "SSEStringMethod" endpoint
+// SSE connection.
+func (s *SSEStringMethodServerStream) Send(v string) error {
+	return s.SendWithContext(context.Background(), v)
+}
+
+// SendWithContext SendWithContext streams instances of "string" to the
+// "SSEStringMethod" endpoint SSE connection with context.
+func (s *SSEStringMethodServerStream) SendWithContext(ctx context.Context, v string) error {
+	s.once.Do(func() {
+		header := s.w.Header()
+		if header.Get("Content-Type") == "" {
+			header.Set("Content-Type", "text/event-stream")
+		}
+		if header.Get("Cache-Control") == "" {
+			header.Set("Cache-Control", "no-cache")
+		}
+		if header.Get("Connection") == "" {
+			header.Set("Connection", "keep-alive")
+		}
+		s.w.WriteHeader(http.StatusOK)
+	})
+	res := v
+
+	var data string
+	data = res
+	fmt.Fprintf(s.w, "data: %s\n\n", data)
+
+	if f, ok := s.w.(http.Flusher); ok {
+		f.Flush()
+	}
+	return nil
+}
+
+// Close is a no-op for SSE. We keep the method for compatibility with other
+// stream types.
+func (s *SSEStringMethodServerStream) Close() error {
+	return nil
+}
diff --git a/http/codegen/testdata/result_encode_functions.go b/http/codegen/testdata/result_encode_functions.go
index 02d402c1..449fee8a 100644
--- a/http/codegen/testdata/result_encode_functions.go
+++ b/http/codegen/testdata/result_encode_functions.go
@@ -1036,6 +1036,16 @@ func EncodeMethodEmptyServerResponseWithTagsResponse(encoder func(context.Contex
 }
 `
 
+var ResponseEncoderSkipResponseBodyEncodeDecodeCode = `// EncodeMethodResponseEncoderSkipResponse returns an encoder for responses
+// returned by the ServiceResponseEncoderSkip MethodResponseEncoderSkip
+// endpoint.
+func EncodeMethodResponseEncoderSkipResponse(encoder func(context.Context, http.ResponseWriter) goahttp.Encoder) func(context.Context, http.ResponseWriter, any) error {
+	return func(ctx context.Context, w http.ResponseWriter, v any) error {
+		return nil
+	}
+}
+`
+
 var ResultWithCustomPkgTypeEncodeCode = `// EncodeMethodResultWithCustomPkgTypeDSLResponse returns an encoder for
 // responses returned by the ServiceResultWithCustomPkgTypeDSL
 // MethodResultWithCustomPkgTypeDSL endpoint.
diff --git a/http/codegen/testdata/server_dsls.go b/http/codegen/testdata/server_dsls.go
index 8f9a3a5f..b50ea913 100644
--- a/http/codegen/testdata/server_dsls.go
+++ b/http/codegen/testdata/server_dsls.go
@@ -294,3 +294,15 @@ var ServerSkipResponseBodyEncodeDecodeDSL = func() {
 		})
 	})
 }
+
+var ResponseEncoderSkipResponseBodyEncodeDecodeDSL = func() {
+	Service("ServiceResponseEncoderSkip", func() {
+		Method("MethodResponseEncoderSkip", func() {
+			HTTP(func() {
+				GET("/")
+				SkipResponseBodyEncodeDecode()
+				Response(StatusOK)
+			})
+		})
+	})
+}
diff --git a/http/codegen/testdata/sse_dsls.go b/http/codegen/testdata/sse_dsls.go
new file mode 100644
index 00000000..d7de8bbf
--- /dev/null
+++ b/http/codegen/testdata/sse_dsls.go
@@ -0,0 +1,142 @@
+package testdata
+
+import (
+	. "goa.design/goa/v3/dsl"
+)
+
+var SSEStringDSL = func() {
+	Service("SSEStringService", func() {
+		Method("SSEStringMethod", func() {
+			StreamingResult(String)
+			HTTP(func() {
+				GET("/string")
+				ServerSentEvents()
+			})
+		})
+	})
+}
+
+var SSEIntDSL = func() {
+	Service("SSEIntService", func() {
+		Method("SSEIntMethod", func() {
+			StreamingResult(Int)
+			HTTP(func() {
+				GET("/int")
+				ServerSentEvents()
+			})
+		})
+	})
+}
+
+var SSEBoolDSL = func() {
+	Service("SSEBoolService", func() {
+		Method("SSEBoolMethod", func() {
+			StreamingResult(Boolean)
+			HTTP(func() {
+				GET("/bool")
+				ServerSentEvents()
+			})
+		})
+	})
+}
+
+var SSEObjectDSL = func() {
+	Service("SSEObjectService", func() {
+		Method("SSEObjectMethod", func() {
+			StreamingResult(func() {
+				Attribute("id", String)
+				Attribute("value", Int)
+				Attribute("flag", Boolean)
+			})
+			HTTP(func() {
+				GET("/object")
+				ServerSentEvents()
+			})
+		})
+	})
+}
+
+var SSEDataFieldDSL = func() {
+	Service("SSEDataFieldService", func() {
+		Method("SSEDataFieldMethod", func() {
+			StreamingResult(func() {
+				Attribute("data", String)
+				Attribute("flag", Boolean)
+			})
+			HTTP(func() {
+				GET("/data-field")
+				ServerSentEvents("data")
+			})
+		})
+	})
+}
+
+var SSEDataIDFieldDSL = func() {
+	Service("SSEDataIDFieldService", func() {
+		Method("SSEDataIDFieldMethod", func() {
+			StreamingResult(func() {
+				Attribute("data", String)
+				Attribute("id", String)
+			})
+			HTTP(func() {
+				GET("/data-id-field")
+				ServerSentEvents("data", func() {
+					SSEEventID("id")
+				})
+			})
+		})
+	})
+}
+
+var SSERequestIDDSL = func() {
+	Service("SSERequestIDService", func() {
+		Method("SSERequestIDMethod", func() {
+			Payload(func() {
+				Attribute("id", String)
+			})
+			StreamingResult(String)
+			HTTP(func() {
+				GET("/request-id")
+				ServerSentEvents(func() {
+					SSERequestID("id")
+				})
+			})
+		})
+	})
+}
+
+var SSEAllFieldsDSL = func() {
+	Service("SSEAllFieldsService", func() {
+		Method("SSEAllFieldsMethod", func() {
+			Payload(func() {
+				Attribute("id", String)
+			})
+			StreamingResult(func() {
+				Attribute("id", String, func() {
+					Example("123")
+				})
+				Attribute("event", String, func() {
+					Example("update")
+				})
+				Attribute("retry", Int, func() {
+					Example(3000)
+				})
+				Attribute("data", func() {
+					Attribute("message", String, func() {
+						Example("Hello, world!")
+					})
+				})
+			})
+			HTTP(func() {
+				GET("/all-fields")
+				ServerSentEvents(func() {
+					SSERequestID("id")
+					SSEEventID("id")
+					SSEEventType("event")
+					SSEEventRetry("retry")
+					SSEEventData("data")
+				})
+			})
+		})
+	})
+}