traefik / yaegi

Yaegi is Another Elegant Go Interpreter

Home Page:https://pkg.go.dev/github.com/traefik/yaegi

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

stdlib._net_http_ResponseWriter is not seen as an http.Hijacker

mpl opened this issue · comments

The following program sample.go triggers an unexpected result

package main

import (
	"bufio"
	"errors"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"net/url"
)

type Middleware struct {
	next http.Handler
}

func (h *Middleware) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	wrw := &WrappedWriter{writer: rw}
	h.next.ServeHTTP(wrw, r)
}

func main() {
	// forwards to e.g. websocat -s 6060
	dest, err := url.Parse("http://localhost:6060")
	if err != nil {
		log.Fatal(err)
	}
	next := httputil.NewSingleHostReverseProxy(dest)
	http.Handle("/", &Middleware{next})
	log.Fatal(http.ListenAndServe(":8080", nil))
}

type WrappedWriter struct {
	writer http.ResponseWriter
}

func (w *WrappedWriter) Header() http.Header {
	return w.writer.Header()
}

func (w *WrappedWriter) Write(buf []byte) (int, error) {
	return w.writer.Write(buf)
}

func (w *WrappedWriter) WriteHeader(statusCode int) {
	w.writer.WriteHeader(statusCode)
}

func (w *WrappedWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
	h, ok := w.writer.(http.Hijacker)
	if !ok {
		return nil, nil, errors.New("hijack not supported")
	}
	return h.Hijack()
}

Expected result

As a backend, run e.g. (from https://github.com/vi/websocat , there's a brew for it):
$ websocat -s 6060

Start the above wrapped reverse proxy:
$ go run ./sample.go

Issue a websocket request on the reverse proxy (which will trigger the hijacking):
$ websocat ws://127.0.0.1:8080/
hello
("hello" should be printed on the backend output)

Got

$ yaegi run ./sample.go

$ websocat ws://127.0.0.1:8080/
websocat: WebSocketError: Received unexpected status code (502 Bad Gateway)
websocat: error running

Error on the yaegi side:
reverseproxy.go:490: http: proxy error: can't switch protocols using non-Hijacker ResponseWriter type stdlib._net_http_ResponseWriter

Yaegi Version

cb642c4 (devel)

Additional Notes

Related traefik issue:

traefik/traefik#8682

Another example, from the same root cause, but with a different impact, and in a different part of the stdlib.
Realistically, in this case it means yaegi probably wouldn't break the big picture behaviour (if the user implementation of WriteTo was honest and not wonky like mine), but it definitely would break some potential optimizations.

package main

import (
	"io"
	"log"
	"os"
	"strings"
)

type WrappedReader struct {
	reader io.Reader
}

func (wr WrappedReader) Read(p []byte) (n int, err error) {
	return wr.reader.Read(p)
}

// Of course, this implementation is completely stupid because it does not write
// to the intended writer, as any honest WriteTo implementation should. its
// implemtion is just to make obvious the divergence of behaviour with yaegi.
func (wr WrappedReader) WriteTo(w io.Writer) (n int64, err error) {
	// Ignore w, send to Stdout to prove whether this WriteTo is used.
	data, err := io.ReadAll(wr)
	if err != nil {
		return 0, err
	}
	nn, err := os.Stdout.Write(data)
	return int64(nn), err
}

func main() {
	f := strings.NewReader("hello world")
	wr := WrappedReader{reader: f}

	// behind the scenes, io.Copy is supposed to use wr.WriteTo if the implementation exists.
	// With Go, it works as expected, i.e. the output is sent to os.Stdout.
	// With Yaegi, it doesn't, i.e. the output is sent to io.Discard.
	if _, err := io.Copy(io.Discard, wr); err != nil {
		log.Fatal(err)
	}
}

Another different/interesting case:

package main

import (
	"image"
	"io"
	"log"
	"os"
)

type WrappedReader struct {
	reader io.Reader
}

func (wr WrappedReader) Read(p []byte) (n int, err error) {
	return wr.reader.Read(p)
}

func (wr WrappedReader) Peek(i int) ([]byte, error) {
	println("BLABLA")
	return []byte("hello"), nil
}

func main() {
	f := strings.NewReader("hello world")
	wr := WrappedReader{reader: f}
	_, format, err := image.Decode(wr)
	if err != nil {
		log.Fatal(err)
	}
	println(format)
}

this one is a little bit different, because the implemented interface is actually a non-exported one this time, in image/format.go:

// A reader is an io.Reader that can also peek ahead.
type reader interface {
	io.Reader
	Peek(int) ([]byte, error)
}

which is used in e.g. asReader, which is called in Decode.

As with the other cases above, the Go compiler sees the interface is being implemented, whereas Yaegi does not.

Edit:

I went through the rest of the image/* package, and I believe these are the other interfaces we'll have to address as well:

in jpeg/writer.go and gif/writer.go , we have the exact counterpart of the reader case above, i.e.:

// writer is a buffered writer.
type writer interface {
	Flush() error
	io.Writer
	io.ByteWriter
}

which is used when encoding images.
Amusingly/interestingly it does not exist for pngs.

On the other hand, we have specific interfaces used in png:

type opaquer interface {
	Opaque() bool
}

In theory, we need to take care of opaquer, because it is asserted when encoding from an image.Image passed as argument, as so:

	if o, ok := m.(opaquer); ok {
		return o.Opaque()
	}

but in practice it is unlikely to be a problem, since the image passed as argument is usually a concrete type coming from the stdlib (e.g. type RGBA) too, which itself implements the Opaque method.

And it's more or less the same situation with:

type PalettedImage interface {
	// ColorIndexAt returns the palette index of the pixel at (x, y).
	ColorIndexAt(x, y int) uint8
	Image
}