Repository: goadesign/goa (go)

Issue:
v3: generated client does not set `Content-Type` header
I've scoured the docs and googled for solutions, but the only thing that came up was #1165. It pretty much perfectly describes the current situation.

If this is something else and specific to the CLI client that's generated by `goa example`, I'd like to know if and what workarounds exist. If this is by design, I'd also like to know that, and maybe there's some documentation that needs updating.

## Repro

1. <details><summary>Implement a simple spec with a single service, e.g.:</summary>

	```golang
	package design

	import . "goa.design/goa/v3/dsl"

	var _ = API("foo", func() {
		HTTP(func() {
			Consumes("application/json")
			Produces("application/json")
		})
	})

	var _ = Service("Bar", func() {
		Method("qux", func() {
			Payload(func() { Attribute("corge", String) })
			Result(func() { Attribute("grault", String) })
			HTTP(func() {
				POST("/")
			})
		})
	})
	```

	</details>

2. <details><summary>Generate a CLI client</summary>

	```shell
	$ goa gen foo/design && \
	  goa example foo/design
	```

	</details>

3. <details><summary>Listen for requests from the client, e.g. using netcat</summary>

	```shell
	$ nc -l 0.0.0.0 8080
	```

	</details>


4. <details><summary>Use the CLI client to make a request</summary>

	```shell
	$ go run foo/cmd/foo-cli -url http://localhost:8080 \
      bar qux -body '{"corge":"garply"}'
	```

	</details>

### What was expected

The CLI client should set the `Content-Type` header to `application/json` when making requests.

### What happens instead

<details><summary>The CLI client performs requests without a <code>Content-Type</code> header.</summary>

```http
POST / HTTP/1.1
Host: localhost:8080
User-Agent: Go-http-client/1.1
Transfer-Encoding: chunked
Accept-Encoding: gzip

{"corge":"garply"}

```

</details>

## Additional details

It happens this way regardless of the presence of `Consumes`/`Produces` in the API DSL.

<details><summary>The generated OpenAPI spec does include the <code>application/json</code> content type as expected: (openapi3.yaml)</summary>

```yaml
openapi: 3.0.3
info:
  title: Goa API
  version: "1.0"
servers:
- url: http://localhost:80
  description: Default server for foo
paths:
  /:
    post:
      tags:
      - Bar
      summary: qux Bar
      operationId: Bar#qux
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/QuxRequestBody'
            example:
              corge: Inventore qui optio perspiciatis.
      responses:
        "200":
          description: OK response.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/QuxResponseBody'
              example:
                grault: Ut aut ut officiis est et et.
components:
  schemas:
    QuxRequestBody:
      type: object
      properties:
        corge:
          type: string
          example: Magni nostrum voluptas sit.
      example:
        corge: At tempore assumenda dolorem officiis tempora tempora.
    QuxResponseBody:
      type: object
      properties:
        grault:
          type: string
          example: Corporis autem consectetur aliquid quia omnis quia.
      example:
        grault: Porro sed explicabo non.
tags:
- name: Bar
```

</details>

thing|version
-|-
go|go1.17.6
goa|v3.5.4
platform|linux/amd64


Pull request:
http: Set request `Content-Type` in the default `RequestEncoder`
Fixes #2982

Patch:
diff --git a/http/encoding.go b/http/encoding.go
index 6448b63f..778af1cf 100644
--- a/http/encoding.go
+++ b/http/encoding.go
@@ -170,6 +170,10 @@ func ResponseEncoder(ctx context.Context, w http.ResponseWriter) Encoder {
 // RequestEncoder returns a HTTP request encoder.
 // The encoder uses package encoding/json.
 func RequestEncoder(r *http.Request) Encoder {
+	const k = "Content-Type"
+	if h := r.Header.Get(k); h == "" {
+		r.Header.Set(k, "application/json")
+	}
 	var buf bytes.Buffer
 	r.Body = ioutil.NopCloser(&buf)
 	return json.NewEncoder(&buf)


Test patch:
diff --git a/http/encoding_test.go b/http/encoding_test.go
index 875bcb10..76604637 100644
--- a/http/encoding_test.go
+++ b/http/encoding_test.go
@@ -13,6 +13,43 @@ var (
 	testString = "test string"
 )
 
+func TestRequestEncoder(t *testing.T) {
+	const (
+		ct      = "Content-Type"
+		ctJSON  = "application/json"
+		ctOther = "<other>"
+		wantT   = "*json.Encoder"
+	)
+	cases := []struct {
+		name      string
+		requestCT string
+		wantCT    string
+	}{
+		{"no ct", "", ctJSON},
+		{"json ct", ctJSON, ctJSON},
+		{"other ct", ctOther, ctOther},
+	}
+	for _, c := range cases {
+		t.Run(c.name, func(t *testing.T) {
+			r := &http.Request{
+				Header: http.Header{},
+			}
+			if c.requestCT != "" {
+				r.Header.Set(ct, c.requestCT)
+			}
+
+			encoder := RequestEncoder(r)
+
+			if gotT := fmt.Sprintf("%T", encoder); gotT != wantT {
+				t.Errorf("got encoder type %s, want %s", gotT, wantT)
+			}
+			if gotCT := r.Header.Get(ct); gotCT != c.wantCT {
+				t.Errorf("got Content-Type %q, want %q", gotCT, c.wantCT)
+			}
+		})
+	}
+}
+
 func TestResponseEncoder(t *testing.T) {
 	cases := []struct {
 		name        string