Skip to content

Commit 92fcabb

Browse files
committed
feat: Handle new location after redirects
After a 301-303/307/308 redirect the location is updated - The user agent updates the location, without adding a history entry for the URL with the redirect. - XHR updates `responseURL` accordingly
2 parents 62696c0 + a97819b commit 92fcabb

File tree

5 files changed

+113
-7
lines changed

5 files changed

+113
-7
lines changed

html/window.go

+28-5
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import (
1616
"github.com/gost-dom/browser/url"
1717
)
1818

19+
var ErrTooManyRedirects = errors.New("Too many redirects")
20+
1921
type ScriptHost interface {
2022
NewContext(window Window) ScriptContext
2123
Close()
@@ -108,6 +110,14 @@ func newWindow(windowOptions ...WindowOption) *window {
108110
return win
109111
}
110112

113+
func (w *window) checkRedirect(req *http.Request, via []*http.Request) error {
114+
if len(via) > 9 {
115+
return ErrTooManyRedirects
116+
}
117+
w.history.ReplaceState(EMPTY_STATE, req.URL.String())
118+
return nil
119+
}
120+
111121
func NewWindow(windowOptions ...WindowOption) Window {
112122
return newWindow(windowOptions...)
113123
}
@@ -209,8 +219,13 @@ func (w *window) Document() dom.Document {
209219
return w.document
210220
}
211221

212-
func (w *window) Navigate(href string) error {
222+
func (w *window) Navigate(href string) (err error) {
213223
log.Info(w.Logger(), "Window.navigate:", "href", href)
224+
defer func() {
225+
if err != nil {
226+
log.Warn(w.logger, "Window.navigate: Error response", "err", err.Error())
227+
}
228+
}()
214229
w.History().pushLoad(href)
215230
w.initScriptEngine()
216231
w.baseLocation = href
@@ -236,15 +251,23 @@ func (w *window) reload(href string) error {
236251
}
237252
}
238253

239-
func (w *window) get(href string) (err error) {
254+
func (w *window) get(href string) error {
240255
if req, err := http.NewRequest("GET", href, nil); err == nil {
241-
err = w.fetchRequest(req)
256+
return w.fetchRequest(req)
257+
} else {
258+
return err
242259
}
243-
return err
244260
}
245261

262+
// fetchRequest handles "User agent" requests. I.e., navigation by clicking a
263+
// link, submitting a form, of calling a JS API that makes the browser itself
264+
// navigate to a new URL.
246265
func (w *window) fetchRequest(req *http.Request) error {
247-
resp, err := w.httpClient.Do(req)
266+
// Create a copy of the client, and set the CheckRedirect to a function that
267+
// updates the window location to reflect the new URL.
268+
client := w.httpClient
269+
client.CheckRedirect = w.checkRedirect
270+
resp, err := client.Do(req)
248271
if err != nil {
249272
return err
250273
}

html/window_test.go

+63
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,20 @@
11
package html_test
22

33
import (
4+
"fmt"
5+
"io"
6+
"net/http"
47
"strings"
58
"testing"
69

710
"github.com/stretchr/testify/suite"
811

912
"github.com/gost-dom/browser/dom"
13+
"github.com/gost-dom/browser/html"
1014
. "github.com/gost-dom/browser/html"
1115
. "github.com/gost-dom/browser/internal/testing/gomega-matchers"
1216
"github.com/gost-dom/browser/internal/testing/gosttest"
17+
"github.com/gost-dom/browser/internal/testing/htmltest"
1318
. "github.com/gost-dom/browser/testing/gomega-matchers"
1419
)
1520

@@ -32,3 +37,61 @@ func (s *WindowTestSuite) TestDocumentWithDOCTYPE() {
3237
func TestWindow(t *testing.T) {
3338
suite.Run(t, new(WindowTestSuite))
3439
}
40+
41+
type WindowNavigationTestSuite struct {
42+
gosttest.GomegaSuite
43+
win htmltest.WindowHelper
44+
}
45+
46+
func (s *WindowNavigationTestSuite) SetupTest() {
47+
m := http.NewServeMux()
48+
m.HandleFunc("GET /page-1", func(w http.ResponseWriter, r *http.Request) {
49+
w.Write([]byte(`<body>
50+
<a id="link" href="/old-page-2">Link</a>
51+
<form method="post" action="/old-page-2" id="form">
52+
<input name="data" value="value" type="text" />
53+
<button type="submit" id="submit">Submit</button>
54+
</form>
55+
</body>`))
56+
})
57+
m.HandleFunc("/old-page-2", func(w http.ResponseWriter, r *http.Request) {
58+
u := r.URL
59+
u.Path = "/new-page-2"
60+
w.Header().Add("Location", u.String())
61+
w.WriteHeader(301)
62+
})
63+
m.HandleFunc("/new-page-2", func(w http.ResponseWriter, r *http.Request) {
64+
body, err := io.ReadAll(r.Body)
65+
if err != nil {
66+
w.WriteHeader(500)
67+
}
68+
respBody := fmt.Sprintf(
69+
`<body><h1>Page 2</h1>
70+
<div id="method">%s</div>
71+
<div id="request-body">%s</div>
72+
</body>`, r.Method, string(body),
73+
)
74+
w.Write([]byte(respBody))
75+
})
76+
m.HandleFunc("/infinite-redirects", func(w http.ResponseWriter, r *http.Request) {
77+
http.Redirect(w, r, "/infinite-redirects", 301)
78+
})
79+
80+
s.win = htmltest.NewWindowHelper(s.T(), NewWindowFromHandler(m))
81+
s.win.Navigate("https://example.com/page-1")
82+
}
83+
84+
func TestWindowNavigation(t *testing.T) {
85+
suite.Run(t, new(WindowNavigationTestSuite))
86+
}
87+
88+
func (s *WindowNavigationTestSuite) TestRedirectGetRequests() {
89+
s.Assert().Equal("/page-1", s.win.Location().Pathname())
90+
s.win.HTMLDocument().GetHTMLElementById("link").Click()
91+
s.Assert().Equal("https://example.com/new-page-2", s.win.Location().Href())
92+
}
93+
94+
func (s *WindowNavigationTestSuite) TestInfinteRedirects() {
95+
err := s.win.Navigate("/infinite-redirects")
96+
s.Assert().ErrorIs(err, html.ErrTooManyRedirects, "Error is too many redirects")
97+
}

internal/gosthttp/test_round_tripper.go

+3-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ func (h TestRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
2424
serverReq.Body = nullReader{}
2525
}
2626
h.ServeHTTP(rec, serverReq)
27-
return rec.Result(), nil
27+
resp := rec.Result()
28+
resp.Request = req
29+
return resp, nil
2830
}
2931

3032
// nullReader is just a reader with no content. When _sending_ an HTTP request,

internal/html/xml_http_request.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func (req *xmlHttpRequest) Status() int { return req.status }
159159
// be in JS wrapper layer
160160
func (req *xmlHttpRequest) StatusText() string { return http.StatusText(req.status) }
161161

162-
func (req *xmlHttpRequest) ResponseURL() string { return req.url }
162+
func (req *xmlHttpRequest) ResponseURL() string { return req.res.Request.URL.String() }
163163

164164
func (req *xmlHttpRequest) Response() string { return req.ResponseText() }
165165

internal/html/xml_http_request_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
. "github.com/gost-dom/browser/internal/html"
1616
. "github.com/gost-dom/browser/internal/testing/gomega-matchers"
1717
"github.com/gost-dom/browser/internal/testing/gosttest"
18+
"github.com/stretchr/testify/assert"
1819
"github.com/stretchr/testify/suite"
1920

2021
"github.com/onsi/gomega/types"
@@ -218,6 +219,23 @@ func (s *XMLHTTPRequestTestSuite) TestCookieVisibility() {
218219
).To(HaveLines("x-test-1: value1", "x-test-2: value2", "content-type: text/plain"))
219220
}
220221

222+
func TestXMLHTTPRequestRedirect(t *testing.T) {
223+
m := http.NewServeMux()
224+
m.Handle("GET /redirect", http.RedirectHandler("/redirect-temp", 301))
225+
m.Handle("GET /redirect-temp", http.RedirectHandler("/redirected-url", 301))
226+
m.HandleFunc("GET /redirected-url", func(w http.ResponseWriter, r *http.Request) {
227+
w.Write([]byte("Handled"))
228+
})
229+
xhr := NewXmlHttpRequest(
230+
stubBrowsingContext{client: gosthttp.NewHttpClientFromHandler(m)},
231+
clock.New(),
232+
)
233+
xhr.Open("GET", "https://example.com/redirect", RequestOptionAsync(false))
234+
xhr.Send()
235+
236+
assert.Equal(t, "https://example.com/redirected-url", xhr.ResponseURL())
237+
}
238+
221239
func HaveLines(expected ...string) types.GomegaMatcher {
222240
return WithTransform(func(s string) []string {
223241
lines := strings.Split(s, "\r\n")

0 commit comments

Comments
 (0)