github.com/symfony-cli/symfony-cli@v0.0.0-20240514161054-ece2df437dfa/local/php/cgi.go (about)

     1  package php
     2  
     3  import (
     4  	"bytes"
     5  	"io"
     6  	"net/http"
     7  	"strconv"
     8  	"time"
     9  
    10  	"github.com/pkg/errors"
    11  	fcgiclient "github.com/symfony-cli/symfony-cli/local/fcgi_client"
    12  )
    13  
    14  type cgiTransport struct{}
    15  
    16  func (p *cgiTransport) RoundTrip(req *http.Request) (*http.Response, error) {
    17  	env := req.Context().Value(environmentContextKey).(map[string]string)
    18  
    19  	// as the process might have been just created, it might not be ready yet
    20  	var fcgi *fcgiclient.FCGIClient
    21  	var err error
    22  	max := 10
    23  	i := 0
    24  	for {
    25  		if fcgi, err = fcgiclient.Dial("tcp", "127.0.0.1:"+req.URL.Port()); err == nil {
    26  			break
    27  		}
    28  		i++
    29  		if i > max {
    30  			return nil, errors.Wrapf(err, "unable to connect to the PHP FastCGI process")
    31  		}
    32  		time.Sleep(time.Millisecond * 50)
    33  	}
    34  
    35  	// The CGI spec doesn't allow chunked requests. Go is already assembling the
    36  	// chunks from the request to a usable Reader (see net/http.readTransfer and
    37  	// net/http/internal.NewChunkedReader), so the only thing we have to
    38  	// do to is get the content length and add it to the header but to do so we
    39  	// have to read and buffer the body content.
    40  	if len(req.TransferEncoding) > 0 && req.TransferEncoding[0] == "chunked" {
    41  		bodyBuffer := &bytes.Buffer{}
    42  		bodyBytes, err := io.Copy(bodyBuffer, req.Body)
    43  		if err != nil {
    44  			return nil, err
    45  		}
    46  
    47  		req.Body = io.NopCloser(bodyBuffer)
    48  		req.TransferEncoding = nil
    49  		env["CONTENT_LENGTH"] = strconv.FormatInt(bodyBytes, 10)
    50  		env["HTTP_CONTENT_LENGTH"] = env["CONTENT_LENGTH"]
    51  	}
    52  
    53  	// fetching the response from the fastcgi backend, and check for errors
    54  	resp, err := fcgi.Request(env, req.Body)
    55  	if err != nil {
    56  		return nil, errors.Wrapf(err, "unable to fetch the response from the backend")
    57  	}
    58  	resp.Body = cgiBodyReadCloser{resp.Body, fcgi}
    59  	resp.Request = req
    60  
    61  	return resp, nil
    62  }
    63  
    64  // cgiBodyReadCloser is responsible for postponing the CGI connection
    65  // termination when the client finished reading the response. This effectively
    66  // allows to "stream" the CGI response from the server to the client by removing
    67  // the requirement for an in-between buffer.
    68  type cgiBodyReadCloser struct {
    69  	io.Reader
    70  	*fcgiclient.FCGIClient
    71  }
    72  
    73  func (f cgiBodyReadCloser) Close() error {
    74  	f.FCGIClient.Close()
    75  	return nil
    76  }