Repository: caddyserver/caddy
File: admin.go (Go)

Code before:
func handleConfig(w http.ResponseWriter, r *http.Request) error {
	switch r.Method {
	case http.MethodGet:
		w.Header().Set("Content-Type", "application/json")
		hash := etagHasher()

		// Read the config into a buffer instead of writing directly to
		// the response writer, as we want to set the ETag as the header,
		// not the trailer.
		buf := bufferPool.Get().(*bytes.Buffer)

		configWriter := io.MultiWriter(buf, hash)
		err := readConfig(r.URL.Path, configWriter)
		if err != nil {
			return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
		}

		// we could consider setting up a sync.Pool for the summed
		// hashes to reduce GC pressure.
		w.Header().Set("Etag", makeEtag(r.URL.Path, hash))
		_, err = w.Write(buf.Bytes())
		if err != nil {
			return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
		}

		buf.Reset()
		bufferPool.Put(buf)

		return nil

	case http.MethodPost,
		http.MethodPut,
		http.MethodPatch,
		http.MethodDelete:

		// DELETE does not use a body, but the others do
		var body []byte
		if r.Method != http.MethodDelete {
			if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
				return APIError{
					HTTPStatus: http.StatusBadRequest,
					Err:        fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
				}
			}

			buf := bufPool.Get().(*bytes.Buffer)
			buf.Reset()
			defer bufPool.Put(buf)

			_, err := io.Copy(buf, r.Body)
			if err != nil {

Review comment (bug):
We should move the `Reset` to when the buffer is retrieved (when client requests invalid config path it will not be recycled and rare chance response fails to write), and `defer` to return the buffer there.

Code after:
func handleConfig(w http.ResponseWriter, r *http.Request) error {
	switch r.Method {
	case http.MethodGet:
		w.Header().Set("Content-Type", "application/json")
		hash := etagHasher()

		// Read the config into a buffer instead of writing directly to
		// the response writer, as we want to set the ETag as the header,
		// not the trailer.
		buf := bufferPool.Get().(*bytes.Buffer)
		buf.Reset()
		defer bufferPool.Put(buf)

		configWriter := io.MultiWriter(buf, hash)
		err := readConfig(r.URL.Path, configWriter)
		if err != nil {
			return APIError{HTTPStatus: http.StatusBadRequest, Err: err}
		}

		// we could consider setting up a sync.Pool for the summed
		// hashes to reduce GC pressure.
		w.Header().Set("Etag", makeEtag(r.URL.Path, hash))
		_, err = w.Write(buf.Bytes())
		if err != nil {
			return APIError{HTTPStatus: http.StatusInternalServerError, Err: err}
		}

		return nil

	case http.MethodPost,
		http.MethodPut,
		http.MethodPatch,
		http.MethodDelete:

		// DELETE does not use a body, but the others do
		var body []byte
		if r.Method != http.MethodDelete {
			if ct := r.Header.Get("Content-Type"); !strings.Contains(ct, "/json") {
				return APIError{
					HTTPStatus: http.StatusBadRequest,
					Err:        fmt.Errorf("unacceptable content-type: %v; 'application/json' required", ct),
				}
			}

			buf := bufPool.Get().(*bytes.Buffer)
			buf.Reset()
			defer bufPool.Put(buf)

			_, err := io.Copy(buf, r.Body)
			if err != nil {
				return APIError{