diff --git a/_examples/basic-auth-proxy/go.mod b/_examples/basic-auth-proxy/go.mod index 08b49d7..214b426 100644 --- a/_examples/basic-auth-proxy/go.mod +++ b/_examples/basic-auth-proxy/go.mod @@ -2,9 +2,6 @@ module github.com/syumai/workers/_examples/basic-auth-server go 1.21.3 -require ( - github.com/syumai/tinyutil v0.3.0 - github.com/syumai/workers v0.5.1 -) +require github.com/syumai/workers v0.5.1 replace github.com/syumai/workers => ../../ diff --git a/_examples/basic-auth-proxy/go.sum b/_examples/basic-auth-proxy/go.sum index 4c072c7..e69de29 100644 --- a/_examples/basic-auth-proxy/go.sum +++ b/_examples/basic-auth-proxy/go.sum @@ -1,2 +0,0 @@ -github.com/syumai/tinyutil v0.3.0 h1:sgWeE8oQyequIRLNeHZgR1PddpY4mxcdkfMgx2m53IE= -github.com/syumai/tinyutil v0.3.0/go.mod h1:/owCyUs1bh6tKxH7K1Ze3M/zZtZ+vGrj3h82fgNHDFI= diff --git a/_examples/basic-auth-proxy/main.go b/_examples/basic-auth-proxy/main.go index 10a9450..53ad163 100644 --- a/_examples/basic-auth-proxy/main.go +++ b/_examples/basic-auth-proxy/main.go @@ -1,12 +1,13 @@ package main import ( + "fmt" "io" "log" "net/http" - "github.com/syumai/tinyutil/httputil" "github.com/syumai/workers" + "github.com/syumai/workers/cloudflare/fetch" ) const ( @@ -33,12 +34,20 @@ func handleRequest(w http.ResponseWriter, req *http.Request) { u := *req.URL u.Scheme = "https" u.Host = "syum.ai" - resp, err := httputil.Get(u.String()) + r, err := fetch.NewRequest(req.Context(), req.Method, u.String(), req.Body) if err != nil { handleError(w, http.StatusInternalServerError, "Internal Error") log.Printf("failed to execute proxy request: %v\n", err) return } + r.Header = req.Header.Clone() + cli := fetch.NewClient() + resp, err := cli.Do(r, nil) + if err != nil { + fmt.Println(err) + w.WriteHeader(http.StatusInternalServerError) + return + } for k, values := range resp.Header { for _, v := range values { w.Header().Add(k, v) diff --git a/cloudflare/kv.go b/cloudflare/kv.go index 2f824f7..6d3b936 100644 --- a/cloudflare/kv.go +++ b/cloudflare/kv.go @@ -66,7 +66,7 @@ func (kv *KVNamespace) GetReader(key string, opts *KVNamespaceGetOptions) (io.Re if err != nil { return nil, err } - return jsutil.ConvertStreamReaderToReader(v.Call("getReader")), nil + return jsutil.ConvertReadableStreamToReadCloser(v), nil } // KVNamespaceListOptions represents Cloudflare KV namespace list options. diff --git a/cloudflare/r2object.go b/cloudflare/r2object.go index 8a26a31..c3fd027 100644 --- a/cloudflare/r2object.go +++ b/cloudflare/r2object.go @@ -54,7 +54,7 @@ func toR2Object(v js.Value) (*R2Object, error) { bodyVal := v.Get("body") var body io.Reader if !bodyVal.IsUndefined() { - body = jsutil.ConvertStreamReaderToReader(v.Get("body").Call("getReader")) + body = jsutil.ConvertReadableStreamToReadCloser(v.Get("body")) } return &R2Object{ instance: v, diff --git a/cloudflare/sockets/socket.go b/cloudflare/sockets/socket.go index 4c80fd7..446c085 100644 --- a/cloudflare/sockets/socket.go +++ b/cloudflare/sockets/socket.go @@ -14,12 +14,13 @@ import ( func newSocket(ctx context.Context, sockVal js.Value, readDeadline, writeDeadline time.Time) *Socket { ctx, cancel := context.WithCancel(ctx) writerVal := sockVal.Get("writable").Call("getWriter") - readerVal := sockVal.Get("readable").Call("getReader") + readerVal := sockVal.Get("readable") + readCloser := jsutil.ConvertReadableStreamToReadCloser(readerVal) return &Socket{ ctx: ctx, cancel: cancel, - reader: jsutil.ConvertStreamReaderToReader(readerVal), + reader: readCloser, writerVal: writerVal, readDeadline: readDeadline, @@ -27,7 +28,7 @@ func newSocket(ctx context.Context, sockVal js.Value, readDeadline, writeDeadlin startTLS: func() js.Value { return sockVal.Call("startTls") }, close: func() { sockVal.Call("close") }, - closeRead: func() { readerVal.Call("close") }, + closeRead: func() { readCloser.Close() }, closeWrite: func() { writerVal.Call("close") }, } } diff --git a/internal/jshttp/request.go b/internal/jshttp/request.go index e38caa0..ae2b0f6 100644 --- a/internal/jshttp/request.go +++ b/internal/jshttp/request.go @@ -17,8 +17,7 @@ func ToBody(streamOrNull js.Value) io.ReadCloser { if streamOrNull.IsNull() { return nil } - sr := streamOrNull.Call("getReader") - return io.NopCloser(jsutil.ConvertStreamReaderToReader(sr)) + return jsutil.ConvertReadableStreamToReadCloser(streamOrNull) } // ToRequest converts JavaScript sides Request to *http.Request. diff --git a/internal/jshttp/response.go b/internal/jshttp/response.go index 41a11a9..3f48c34 100644 --- a/internal/jshttp/response.go +++ b/internal/jshttp/response.go @@ -25,19 +25,19 @@ func ToResponse(res js.Value) (*http.Response, error) { Status: strconv.Itoa(status) + " " + res.Get("statusText").String(), StatusCode: status, Header: header, - Body: io.NopCloser(jsutil.ConvertStreamReaderToReader(blob.Call("stream").Call("getReader"))), + Body: jsutil.ConvertReadableStreamToReadCloser(blob.Call("stream")), ContentLength: contentLength, }, nil } // ToJSResponse converts *http.Response to JavaScript sides Response class object. func ToJSResponse(res *http.Response) js.Value { - return newJSResponse(res.StatusCode, res.Header, res.Body) + return newJSResponse(res.StatusCode, res.Header, res.Body, nil) } // newJSResponse creates JavaScript sides Response class object. // - Response: https://developer.mozilla.org/docs/Web/API/Response -func newJSResponse(statusCode int, headers http.Header, body io.ReadCloser) js.Value { +func newJSResponse(statusCode int, headers http.Header, body io.ReadCloser, rawBody *js.Value) js.Value { status := statusCode if status == 0 { status = http.StatusOK @@ -52,6 +52,11 @@ func newJSResponse(statusCode int, headers http.Header, body io.ReadCloser) js.V status == http.StatusNotModified { return jsutil.ResponseClass.New(jsutil.Null, respInit) } - readableStream := jsutil.ConvertReaderToReadableStream(body) + var readableStream js.Value + if rawBody != nil { + readableStream = *rawBody + } else { + readableStream = jsutil.ConvertReaderToReadableStream(body) + } return jsutil.ResponseClass.New(readableStream, respInit) } diff --git a/internal/jshttp/responsewriter.go b/internal/jshttp/responsewriter.go index fe3dbac..51f4843 100644 --- a/internal/jshttp/responsewriter.go +++ b/internal/jshttp/responsewriter.go @@ -5,6 +5,8 @@ import ( "net/http" "sync" "syscall/js" + + "github.com/syumai/workers/internal/jsutil" ) type ResponseWriter struct { @@ -14,9 +16,13 @@ type ResponseWriter struct { Writer *io.PipeWriter ReadyCh chan struct{} Once sync.Once + RawJSBody *js.Value } -var _ http.ResponseWriter = &ResponseWriter{} +var ( + _ http.ResponseWriter = (*ResponseWriter)(nil) + _ jsutil.RawJSBodyWriter = (*ResponseWriter)(nil) +) // Ready indicates that ResponseWriter is ready to be converted to Response. func (w *ResponseWriter) Ready() { @@ -38,8 +44,12 @@ func (w *ResponseWriter) WriteHeader(statusCode int) { w.StatusCode = statusCode } +func (w *ResponseWriter) WriteRawJSBody(body js.Value) { + w.RawJSBody = &body +} + // ToJSResponse converts *ResponseWriter to JavaScript sides Response. // - Response: https://developer.mozilla.org/docs/Web/API/Response func (w *ResponseWriter) ToJSResponse() js.Value { - return newJSResponse(w.StatusCode, w.HeaderValue, w.Reader) + return newJSResponse(w.StatusCode, w.HeaderValue, w.Reader, w.RawJSBody) } diff --git a/internal/jsutil/stream.go b/internal/jsutil/stream.go index 60120b8..9b0c59d 100644 --- a/internal/jsutil/stream.go +++ b/internal/jsutil/stream.go @@ -7,16 +7,30 @@ import ( "syscall/js" ) -// streamReaderToReader implements io.Reader sourced from ReadableStreamDefaultReader. +type RawJSBodyWriter interface { + WriteRawJSBody(body js.Value) +} + +// readableStreamToReadCloser implements io.Reader sourced from ReadableStreamDefaultReader. // - ReadableStreamDefaultReader: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStreamDefaultReader // - This implementation is based on: https://deno.land/std@0.139.0/streams/conversion.ts#L76 -type streamReaderToReader struct { +type readableStreamToReadCloser struct { buf bytes.Buffer - streamReader js.Value + stream js.Value + streamReader *js.Value } +var ( + _ io.ReadCloser = (*readableStreamToReadCloser)(nil) + _ io.WriterTo = (*readableStreamToReadCloser)(nil) +) + // Read reads bytes from ReadableStreamDefaultReader. -func (sr *streamReaderToReader) Read(p []byte) (n int, err error) { +func (sr *readableStreamToReadCloser) Read(p []byte) (n int, err error) { + if sr.streamReader == nil { + r := sr.stream.Call("getReader") + sr.streamReader = &r + } if sr.buf.Len() == 0 { promise := sr.streamReader.Call("read") resultCh := make(chan js.Value) @@ -56,10 +70,31 @@ func (sr *streamReaderToReader) Read(p []byte) (n int, err error) { return sr.buf.Read(p) } -// ConvertStreamReaderToReader converts ReadableStreamDefaultReader to io.Reader. -func ConvertStreamReaderToReader(sr js.Value) io.Reader { - return &streamReaderToReader{ - streamReader: sr, +func (sr *readableStreamToReadCloser) Close() error { + if sr.streamReader == nil { + return nil + } + sr.streamReader.Call("close") + return nil +} + +// readerWrapper is wrapper to disable readableStreamToReadCloser's WriteTo method. +type readerWrapper struct { + io.Reader +} + +func (sr *readableStreamToReadCloser) WriteTo(w io.Writer) (n int64, err error) { + if w, ok := w.(RawJSBodyWriter); ok { + w.WriteRawJSBody(sr.stream) + return 0, nil + } + return io.Copy(w, &readerWrapper{sr}) +} + +// ConvertReadableStreamToReadCloser converts ReadableStream to io.ReadCloser. +func ConvertReadableStreamToReadCloser(stream js.Value) io.ReadCloser { + return &readableStreamToReadCloser{ + stream: stream, } }