Ctrl K
ComunidadGuías…

streaming.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package aastro

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"time"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/codes"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/zap"

	"github.com/voidrunner3074/aastro/internal/tracing"
	"github.com/voidrunner3074/aastro/sdk"
)

const streamBuffer = 16 * 1024

type proxyCapable interface {
	proxy(ctx context.Context, w http.ResponseWriter, original *http.Request) error
}

type trackingWriter struct {
	http.ResponseWriter
	written    bool
	statusCode int
}

func (tw *trackingWriter) WriteHeader(code int) {
	tw.written = true
	tw.statusCode = code
	tw.ResponseWriter.WriteHeader(code)
}

func (tw *trackingWriter) Write(b []byte) (int, error) {
	tw.written = true

	if tw.statusCode == 0 {
		tw.statusCode = http.StatusOK
	}

	return tw.ResponseWriter.Write(b)
}

func (tw *trackingWriter) Flush() {
	if f, ok := tw.ResponseWriter.(http.Flusher); ok {
		f.Flush()
	}
}

// handleStreaming streams a request directly to the single upstream without buffering,
// enabling SSE and chunked transfer. Request plugins still run.
func (r *Router) handleStreaming(w http.ResponseWriter, req *http.Request, f *flow, log *zap.Logger) {
	if len(f.upstreams) != 1 {
		log.Error("streaming flow must have exactly one upstream",
			zap.Int("configured", len(f.upstreams)),
		)
		WriteError(w, ClientErrInternal, http.StatusInternalServerError)

		return
	}

	u := f.upstreams[0]

	proxy, ok := u.(proxyCapable)
	if !ok {
		log.Error("upstream does not support streaming (does not implement proxyCapable)",
			zap.String("upstream", u.name()),
		)
		WriteError(w, ClientErrInternal, http.StatusInternalServerError)

		return
	}

	rc := http.NewResponseController(w)
	if err := rc.SetWriteDeadline(time.Time{}); err != nil {
		log.Warn("cannot disable write deadline for streaming", zap.Error(err))
	}
	if err := rc.SetReadDeadline(time.Time{}); err != nil {
		log.Warn("cannot disable read deadline for streaming", zap.Error(err))
	}

	actx := newContext(req)

	if !r.executePlugins(sdk.PluginTypeRequest, w, actx, f, log) {
		return
	}

	tracer := otel.Tracer(tracing.TracerName)
	ctx, span := tracer.Start(actx.Request().Context(), "aastro.upstream",
		trace.WithSpanKind(trace.SpanKindClient),
		trace.WithAttributes(
			attribute.String("aastro.upstream.name", u.name()),
			attribute.String("aastro.upstream.mode", "streaming"),
			attribute.String("aastro.flow.path", f.path),
		),
	)
	defer span.End()

	tw := &trackingWriter{ResponseWriter: w}

	err := proxy.proxy(ctx, tw, actx.Request())
	if err != nil {
		span.RecordError(err)
		span.SetStatus(codes.Error, "streaming upstream error")

		if !tw.written {
			WriteError(w, ClientErrUpstreamUnavailable, http.StatusBadGateway)
		}

		log.Error("streaming upstream error", zap.Error(err))
	}

	if tw.statusCode != 0 {
		span.SetAttributes(attribute.Int("http.status_code", tw.statusCode))
		if tw.statusCode >= http.StatusInternalServerError && err == nil {
			span.SetStatus(codes.Error, http.StatusText(tw.statusCode))
		}
	}
}

// streamCopy copies src to dst, flushing after each read if the writer supports it.
// buf size of 16 KiB is intentional: small enough for SSE events, large enough for chunked blobs.
func streamCopy(dst http.ResponseWriter, src io.Reader) error {
	flusher, canFlush := dst.(http.Flusher)

	buf := make([]byte, streamBuffer)

	for {
		n, readErr := src.Read(buf)
		if n > 0 {
			if _, writeErr := dst.Write(buf[:n]); writeErr != nil {
				return fmt.Errorf("write to client: %w", writeErr)
			}

			if canFlush {
				flusher.Flush()
			}
		}

		if readErr == io.EOF {
			return nil
		}

		if readErr != nil {
			return fmt.Errorf("read from upstream: %w", readErr)
		}
	}
}