Repository: goadesign/goa
Pull request #3319 (2023-07-24 17:48:00)

Handle reader errors in SkipResponseBodyEncodeDecode
Once we've started writing the body, it's too late for normal error handling. Instead issue the sentinal panic(http.ErrAbortHandler) to interrupt the stream.

Since the interrupted stream is much less meaningful than the normal error handling, and servers using SkipResponseBodyEncodeDecode may issue errors early in the stream, read a chunk of the stream looking for an error. If encountered, use normal error handling. Otherwise write the chunk plus whatever remains.

For context, I'm attempting to use Transfer-Encoding: Chunked. Go does this for us, automatically, so long as Content-Length hasn't been specified. But that also means there's no way for it to know if the writes were complete. See related documentation at https://stackoverflow.com/questions/40291861/how-to-break-a-chunked-in-go-using-net-http and https://github.com/golang/go/issues/17790.

raphael: Thank you for raising this issue and doing the discovery work! didn't know about the special panic value for breaking chunked responses. This looks good to me, I agree a test would be really nice, maybe a new test case in `server_encode_test.go`? Alternatively it could be a brand new test file `skip_encode_decode_test.go`. The test would generate the code and compare it to golden files content like `server_encode_test.go` does.
MichaelUrman: It looks like handler_test was a better option for the Handler function. Since the behavior is so finicky, I'd feel a lot safer with some actual tests of that. Does goa have any run-time tests I can pattern off of? And/or do you prefer to stick with just golden output tests?
raphael: There isn't any runtime test at the moment but not opposed to adding some. An alternative could be to add an example to github.com/goadesign/example that illustrates this use case as the examples do get tested on CI. The last commit looks great.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3d90f086..e8b1f07a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,7 +8,7 @@ jobs:
     steps:
 
     - name: Set up Go 1.20
-      uses: actions/setup-go@v4.0.1
+      uses: actions/setup-go@v4.1.0
       with:
         go-version: '1.20'
       id: go
@@ -35,7 +35,7 @@ jobs:
     steps:
 
     - name: Set up Go 1.20
-      uses: actions/setup-go@v4.0.1
+      uses: actions/setup-go@v4.1.0
       with:
         go-version: '1.20'
       id: go
diff --git a/README.md b/README.md
index 8e0181aa..4fc80d83 100644
--- a/README.md
+++ b/README.md
@@ -37,7 +37,7 @@ promoting reuse and standardization across services.
         <td>
             <img width="1000" height="0" />
             <a href="https://www.incident.io">
-                <img src="https://incident.io/static/5446bb1376cd3180ade3fefe74859e4b/288ca/logo-social-dark.png" alt="incident.io" width="260" align="right" />
+                <img src="https://incident.io/_next/static/media/logo-social-dark.6a523ace.png" alt="incident.io" width="260" align="right" />
             </a>
             <h3>incident.io: Bounce back stronger after every incident</h3>
             <p>
diff --git a/expr/example.go b/expr/example.go
index a8a5661a..1b0e44cd 100644
--- a/expr/example.go
+++ b/expr/example.go
@@ -6,8 +6,8 @@ import (
 	"regexp"
 	"time"
 
+	regen "github.com/AnatolyRugalev/goregen"
 	googleuuid "github.com/google/uuid"
-	regen "github.com/zach-klippenstein/goregen"
 )
 
 const (
diff --git a/go.mod b/go.mod
index 70c7c4fe..1f8aba1d 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module goa.design/goa/v3
 go 1.20
 
 require (
+	github.com/AnatolyRugalev/goregen v0.1.0
 	github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598
 	github.com/dimfeld/httptreemux/v5 v5.5.0
 	github.com/getkin/kin-openapi v0.118.0
@@ -11,10 +12,9 @@ require (
 	github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d
 	github.com/pkg/errors v0.9.1
 	github.com/sergi/go-diff v1.3.1
-	github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea
-	golang.org/x/text v0.11.0
-	golang.org/x/tools v0.11.0
-	google.golang.org/grpc v1.56.2
+	golang.org/x/text v0.12.0
+	golang.org/x/tools v0.12.0
+	google.golang.org/grpc v1.57.0
 	google.golang.org/protobuf v1.31.0
 	gopkg.in/yaml.v3 v3.0.1
 )
@@ -23,17 +23,15 @@ require (
 	github.com/go-openapi/jsonpointer v0.19.5 // indirect
 	github.com/go-openapi/swag v0.19.13 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
-	github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 // indirect
 	github.com/invopop/yaml v0.1.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
 	github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b // indirect
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/perimeterx/marshmallow v1.1.4 // indirect
-	github.com/smartystreets/goconvey v1.7.2 // indirect
 	golang.org/x/mod v0.12.0 // indirect
-	golang.org/x/net v0.12.0 // indirect
-	golang.org/x/sys v0.10.0 // indirect
+	golang.org/x/net v0.14.0 // indirect
+	golang.org/x/sys v0.11.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 )
diff --git a/go.sum b/go.sum
index ef5e6d63..64c6ee54 100644
--- a/go.sum
+++ b/go.sum
@@ -1,3 +1,5 @@
+github.com/AnatolyRugalev/goregen v0.1.0 h1:xrdXkLaskMnbxW0x4FWNj2yoednv0X2bcTBWpuJGYfE=
+github.com/AnatolyRugalev/goregen v0.1.0/go.mod h1:sVlY1tjcirqLBRZnCcIq1+7/Lwmqz5g7IK8AStjOVzI=
 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -20,12 +22,8 @@ github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4 h1:OL2d27ueTKnlQJoqLW2fc9pWYulFnJYLWzomGV7HqZo=
-github.com/google/gxui v0.0.0-20151028112939-f85e0a97b3a4/go.mod h1:Pw1H1OjSNHiqeuxAduB1BKYXIwFtsyrY47nEqSgEiCM=
 github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
-github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
 github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
 github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
 github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@@ -33,8 +31,6 @@ github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
 github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
-github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
-github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
@@ -61,10 +57,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
 github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
-github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs=
-github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
-github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg=
-github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -79,29 +71,22 @@ github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
 github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
 github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
 github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
-github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea h1:CyhwejzVGvZ3Q2PSbQ4NRRYn+ZWv5eS1vlaEusT+bAI=
-github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea/go.mod h1:eNr558nEUjP8acGw8FFjTeWvSgU1stO7FAO6eknhHe4=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
-golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
-golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
 golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
-golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
-golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
-golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
-golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
-golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
+golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss=
+golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529 h1:DEH99RbiLZhMxrpEJCZ0A+wdTe0EOgou/poSLx9vWf4=
 google.golang.org/genproto/googleapis/rpc v0.0.0-20230629202037-9506855d4529/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
-google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
-google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
+google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
+google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
 google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
 google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
diff --git a/http/codegen/server.go b/http/codegen/server.go
index e26c5e9a..3ba6275f 100644
--- a/http/codegen/server.go
+++ b/http/codegen/server.go
@@ -45,6 +45,7 @@ func serverFile(genpkg string, svc *expr.HTTPServiceExpr) *codegen.File {
 	}
 	sections := []*codegen.SectionTemplate{
 		codegen.Header(title, "server", []*codegen.ImportSpec{
+			{Path: "bufio"},
 			{Path: "context"},
 			{Path: "fmt"},
 			{Path: "io"},
@@ -496,7 +497,7 @@ func {{ .HandlerInit }}(
 	{{- if not .Redirect }}
 		if err != nil {
 			{{- if isWebSocketEndpoint . }}
-			if _, werr := w.Write(nil); werr == http.ErrHijacked {
+			if v.Stream.(*{{ .ServerWebSocket.VarName }}).conn != nil {
 				// Response writer has been hijacked, do not encode the error
 				errhandler(ctx, w, err)
 				return
@@ -511,6 +512,14 @@ func {{ .HandlerInit }}(
 	{{- if .Method.SkipResponseBodyEncodeDecode }}
 		o := res.(*{{ .ServicePkgName }}.{{ .Method.ResponseStruct }})
 		defer o.Body.Close()
+		// handle immediate read error like a returned error
+		buf := bufio.NewReader(o.Body)
+		if _, err := buf.Peek(1); err != nil && err != io.EOF {
+			if err := encodeError(ctx, w, err); err != nil {
+				errhandler(ctx, w, err)
+			}
+			return
+		}
 	{{- end }}
 	{{- if not (or .Redirect (isWebSocketEndpoint .)) }}
 		if err := encodeResponse(ctx, w, {{ if and .Method.SkipResponseBodyEncodeDecode .Result.Ref }}o.Result{{ else }}res{{ end }}); err != nil {
@@ -521,10 +530,11 @@ func {{ .HandlerInit }}(
 		}
 	{{- end }}
 	{{- if .Method.SkipResponseBodyEncodeDecode }}
-		if _, err := io.Copy(w, o.Body); err != nil {
-			if err := encodeError(ctx, w, err); err != nil {
-				errhandler(ctx, w, err)
+		if _, err := io.Copy(w, buf); err != nil {
+			if f, ok := w.(http.Flusher); ok {
+				f.Flush()
 			}
+			panic(http.ErrAbortHandler) // too late to write an error
 		}
 	{{- end }}
 	})


Test patch:
diff --git a/http/codegen/handler_test.go b/http/codegen/handler_test.go
index a7678a96..42112129 100644
--- a/http/codegen/handler_test.go
+++ b/http/codegen/handler_test.go
@@ -24,6 +24,7 @@ func TestHandlerInit(t *testing.T) {
 		{"no payload result", testdata.ServerNoPayloadResultDSL, testdata.ServerNoPayloadResultHandlerConstructorCode},
 		{"payload result", testdata.ServerPayloadResultDSL, testdata.ServerPayloadResultHandlerConstructorCode},
 		{"payload result error", testdata.ServerPayloadResultErrorDSL, testdata.ServerPayloadResultErrorHandlerConstructorCode},
+		{"skip response body encode decode", testdata.ServerSkipResponseBodyEncodeDecodeDSL, testdata.ServerSkipResponseBodyEncodeDecodeCode},
 	}
 	for _, c := range cases {
 		t.Run(c.Name, func(t *testing.T) {
diff --git a/http/codegen/testdata/handler_init_functions.go b/http/codegen/testdata/handler_init_functions.go
index 28bb26a3..657d5bf6 100644
--- a/http/codegen/testdata/handler_init_functions.go
+++ b/http/codegen/testdata/handler_init_functions.go
@@ -241,3 +241,54 @@ func NewMethodPayloadResultErrorHandler(
 	})
 }
 `
+
+var ServerSkipResponseBodyEncodeDecodeCode = `// NewMethodSkipResponseBodyEncodeDecodeHandler creates a HTTP handler which
+// loads the HTTP request and calls the "ServiceSkipResponseBodyEncodeDecode"
+// service "MethodSkipResponseBodyEncodeDecode" endpoint.
+func NewMethodSkipResponseBodyEncodeDecodeHandler(
+	endpoint goa.Endpoint,
+	mux goahttp.Muxer,
+	decoder func(*http.Request) goahttp.Decoder,
+	encoder func(context.Context, http.ResponseWriter) goahttp.Encoder,
+	errhandler func(context.Context, http.ResponseWriter, error),
+	formatter func(ctx context.Context, err error) goahttp.Statuser,
+) http.Handler {
+	var (
+		encodeResponse = EncodeMethodSkipResponseBodyEncodeDecodeResponse(encoder)
+		encodeError    = EncodeMethodSkipResponseBodyEncodeDecodeError(encoder, formatter)
+	)
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		ctx := context.WithValue(r.Context(), goahttp.AcceptTypeKey, r.Header.Get("Accept"))
+		ctx = context.WithValue(ctx, goa.MethodKey, "MethodSkipResponseBodyEncodeDecode")
+		ctx = context.WithValue(ctx, goa.ServiceKey, "ServiceSkipResponseBodyEncodeDecode")
+		var err error
+		res, err := endpoint(ctx, nil)
+		if err != nil {
+			if err := encodeError(ctx, w, err); err != nil {
+				errhandler(ctx, w, err)
+			}
+			return
+		}
+		o := res.(*serviceskipresponsebodyencodedecode.MethodSkipResponseBodyEncodeDecodeResponseData)
+		defer o.Body.Close()
+		// handle immediate read error like a returned error
+		buf := bufio.NewReader(o.Body)
+		if _, err := buf.Peek(1); err != nil && err != io.EOF {
+			if err := encodeError(ctx, w, err); err != nil {
+				errhandler(ctx, w, err)
+			}
+			return
+		}
+		if err := encodeResponse(ctx, w, res); err != nil {
+			errhandler(ctx, w, err)
+			return
+		}
+		if _, err := io.Copy(w, buf); err != nil {
+			if f, ok := w.(http.Flusher); ok {
+				f.Flush()
+			}
+			panic(http.ErrAbortHandler) // too late to write an error
+		}
+	})
+}
+`
diff --git a/http/codegen/testdata/server_dsls.go b/http/codegen/testdata/server_dsls.go
index 99164249..440d0397 100644
--- a/http/codegen/testdata/server_dsls.go
+++ b/http/codegen/testdata/server_dsls.go
@@ -268,3 +268,17 @@ var ServerSimpleRoutingWithRedirectDSL = func() {
 		})
 	})
 }
+
+var ServerSkipResponseBodyEncodeDecodeDSL = func() {
+	Service("ServiceSkipResponseBodyEncodeDecode", func() {
+		Method("MethodSkipResponseBodyEncodeDecode", func() {
+			Error("internal_error", ErrorResult, "Fault while processing download.")
+			HTTP(func() {
+				GET("/")
+				SkipResponseBodyEncodeDecode()
+				Response(StatusOK)
+				Response("internal_error", StatusInternalServerError)
+			})
+		})
+	})
+}
diff --git a/http/codegen/testdata/streaming_code.go b/http/codegen/testdata/streaming_code.go
index b075ca6e..003bbcd4 100644
--- a/http/codegen/testdata/streaming_code.go
+++ b/http/codegen/testdata/streaming_code.go
@@ -58,7 +58,7 @@ func NewStreamingResultMethodHandler(
 		}
 		_, err = endpoint(ctx, v)
 		if err != nil {
-			if _, werr := w.Write(nil); werr == http.ErrHijacked {
+			if v.Stream.(*StreamingResultMethodServerStream).conn != nil {
 				// Response writer has been hijacked, do not encode the error
 				errhandler(ctx, w, err)
 				return
@@ -195,7 +195,7 @@ func NewStreamingResultNoPayloadMethodHandler(
 		}
 		_, err = endpoint(ctx, v)
 		if err != nil {
-			if _, werr := w.Write(nil); werr == http.ErrHijacked {
+			if v.Stream.(*StreamingResultNoPayloadMethodServerStream).conn != nil {
 				// Response writer has been hijacked, do not encode the error
 				errhandler(ctx, w, err)
 				return
@@ -943,7 +943,7 @@ func NewStreamingPayloadMethodHandler(
 		}
 		_, err = endpoint(ctx, v)
 		if err != nil {
-			if _, werr := w.Write(nil); werr == http.ErrHijacked {
+			if v.Stream.(*StreamingPayloadMethodServerStream).conn != nil {
 				// Response writer has been hijacked, do not encode the error
 				errhandler(ctx, w, err)
 				return
@@ -1104,7 +1104,7 @@ func NewStreamingPayloadNoPayloadMethodHandler(
 		}
 		_, err = endpoint(ctx, v)
 		if err != nil {
-			if _, werr := w.Write(nil); werr == http.ErrHijacked {
+			if v.Stream.(*StreamingPayloadNoPayloadMethodServerStream).conn != nil {
 				// Response writer has been hijacked, do not encode the error
 				errhandler(ctx, w, err)
 				return
@@ -2089,7 +2089,7 @@ func NewBidirectionalStreamingMethodHandler(
 		}
 		_, err = endpoint(ctx, v)
 		if err != nil {
-			if _, werr := w.Write(nil); werr == http.ErrHijacked {
+			if v.Stream.(*BidirectionalStreamingMethodServerStream).conn != nil {
 				// Response writer has been hijacked, do not encode the error
 				errhandler(ctx, w, err)
 				return
@@ -2291,7 +2291,7 @@ func NewBidirectionalStreamingNoPayloadMethodHandler(
 		}
 		_, err = endpoint(ctx, v)
 		if err != nil {
-			if _, werr := w.Write(nil); werr == http.ErrHijacked {
+			if v.Stream.(*BidirectionalStreamingNoPayloadMethodServerStream).conn != nil {
 				// Response writer has been hijacked, do not encode the error
 				errhandler(ctx, w, err)
 				return