Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3466,6 +3466,39 @@ func TestMain(t *testing.T) {
assertBufNotContains(t, res.stderr, "POST /chat")
})

t.Run("webtransport dry-run uses effective CONNECT method", func(t *testing.T) {
t.Parallel()
configHome := t.TempDir()
for _, test := range []struct {
name string
args []string
body bool
}{
{name: "default", args: []string{"--webtransport", "--dry-run", "https://example.com/path"}},
{name: "body-inferred POST", args: []string{"--webtransport", "--dry-run", "-d", "payload", "https://example.com/path"}, body: true},
{name: "explicit POST", args: []string{"--webtransport", "--dry-run", "-X", "POST", "https://example.com/path"}},
} {
t.Run(test.name, func(t *testing.T) {
res := runFetchOpts(t, fetchPath, fetchOpts{env: []string{
"HOME=" + configHome,
"XDG_CONFIG_HOME=" + filepath.Join(configHome, "config"),
"HTTP_PROXY=", "http_proxy=", "HTTPS_PROXY=", "https_proxy=",
"ALL_PROXY=", "all_proxy=", "NO_PROXY=*", "no_proxy=*",
}}, test.args...)
assertExitCode(t, 0, res)
assertBufContains(t, res.stderr, "CONNECT /path HTTP/3.0")
assertBufNotContains(t, res.stderr, "GET /path")
assertBufNotContains(t, res.stderr, "POST /path")
if test.body {
assertBufContains(t, res.stderr, "payload")
}
if test.name == "explicit POST" {
assertBufContains(t, res.stderr, "ignoring method POST")
}
})
}
})

t.Run("websocket ctrl-c exits", func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" {
Expand Down
16 changes: 11 additions & 5 deletions internal/fetch/webtransport.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,18 @@ import (
"github.com/ryanfowler/fetch/internal/wt"
)

func handleWebTransport(ctx context.Context, r *Request, c *client.Client, req *http.Request) (int, error) {
if r.MethodExplicit && req.Method != http.MethodConnect {
p := r.PrinterHandle.Stderr()
core.WriteWarningMsgIf(p, "WebTransport requires CONNECT; ignoring method "+req.Method, r.Verbosity == core.VSilent)
req.Method = http.MethodConnect
func setWebTransportMethod(req *http.Request, explicit bool, p *core.Printer, silent bool) {
if req.Method == http.MethodConnect {
return
}
if explicit {
core.WriteWarningMsgIf(p, "WebTransport requires CONNECT; ignoring method "+req.Method, silent)
}
req.Method = http.MethodConnect
}

func handleWebTransport(ctx context.Context, r *Request, c *client.Client, req *http.Request) (int, error) {
setWebTransportMethod(req, r.MethodExplicit, r.PrinterHandle.Stderr(), r.Verbosity == core.VSilent)
p := r.PrinterHandle.Stderr()
if r.Timing {
core.WriteWarningMsgIf(p, "--timing is not supported for WebTransport connections", r.Verbosity == core.VSilent)
Expand Down
124 changes: 124 additions & 0 deletions internal/fetch/webtransport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package fetch

import (
"net/http"
"strings"
"testing"
"time"

"github.com/ryanfowler/fetch/internal/aws"
"github.com/ryanfowler/fetch/internal/client"
"github.com/ryanfowler/fetch/internal/core"
)

func TestSetWebTransportMethod(t *testing.T) {
tests := []struct {
name string
method string
explicit bool
warning string
}{
{name: "default GET", method: http.MethodGet},
{name: "body-inferred POST", method: http.MethodPost},
{name: "explicit non-CONNECT", method: http.MethodPut, explicit: true, warning: "ignoring method PUT"},
{name: "explicit CONNECT", method: http.MethodConnect, explicit: true},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
req, err := http.NewRequest(test.method, "https://example.com/path", nil)
if err != nil {
t.Fatal(err)
}
p := core.TestPrinter(false)

setWebTransportMethod(req, test.explicit, p, false)

if req.Method != http.MethodConnect {
t.Fatalf("method = %q, want CONNECT", req.Method)
}
output := string(p.Bytes())
if test.warning != "" && !strings.Contains(output, test.warning) {
t.Fatalf("warning output = %q, want %q", output, test.warning)
}
if test.warning == "" && output != "" {
t.Fatalf("unexpected warning output: %q", output)
}
})
}
}

func TestSetWebTransportMethodUsesInferredBodyMethod(t *testing.T) {
c := client.NewClient(client.ClientConfig{})
defer c.Close()

req, err := c.NewRequest(t.Context(), client.RequestConfig{
Data: strings.NewReader("payload"),
URL: mustParseURL("https://example.com/path"),
Method: "",
})
if err != nil {
t.Fatal(err)
}
if req.Method != http.MethodPost {
t.Fatalf("inferred method = %q, want POST", req.Method)
}

setWebTransportMethod(req, false, core.TestPrinter(false), false)
if req.Method != http.MethodConnect {
t.Fatalf("method = %q, want CONNECT", req.Method)
}
}

func TestWebTransportAWSMethodSigningUsesCONNECT(t *testing.T) {
cfg := aws.Config{
AccessKey: "AKIDEXAMPLE",
SecretKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
Region: "us-east-1",
Service: "execute-api",
}
when := time.Date(2025, 1, 2, 3, 4, 5, 0, time.UTC)

req, err := http.NewRequest(http.MethodGet, "https://example.com/path", nil)
if err != nil {
t.Fatal(err)
}
setWebTransportMethod(req, false, core.TestPrinter(false), false)
if err := aws.Sign(req, cfg, when); err != nil {
t.Fatal(err)
}
got := req.Header.Get("Authorization")

connectReq, _ := http.NewRequest(http.MethodConnect, "https://example.com/path", nil)
if err := aws.Sign(connectReq, cfg, when); err != nil {
t.Fatal(err)
}
if got != connectReq.Header.Get("Authorization") {
t.Fatalf("signature was not calculated for CONNECT: got %q, want %q", got, connectReq.Header.Get("Authorization"))
}

getReq, _ := http.NewRequest(http.MethodGet, "https://example.com/path", nil)
if err := aws.Sign(getReq, cfg, when); err != nil {
t.Fatal(err)
}
if got == getReq.Header.Get("Authorization") {
t.Fatal("CONNECT and GET signatures are equal")
}
}

func TestWebTransportDryRunMetadataUsesCONNECT(t *testing.T) {
req, err := http.NewRequest(http.MethodGet, "https://example.com/path", nil)
if err != nil {
t.Fatal(err)
}
setWebTransportMethod(req, false, core.TestPrinter(false), false)

p := core.TestPrinter(false)
printRequestMetadataWithURL(p, req, core.HTTP3, core.VSilent, true)
if got := string(p.Bytes()); !strings.Contains(got, "CONNECT /path HTTP/3.0") {
t.Fatalf("dry-run metadata = %q, want CONNECT request line", got)
}
if strings.Contains(string(p.Bytes()), "GET /path") {
t.Fatalf("dry-run metadata contains stale GET request line: %q", p.Bytes())
}
}
Loading