-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This is implementation of http.request, but it returns a promise and does all the networking off the event loop. The code is pretty simple and seems fairly safe given that most possible race conditions would also happen in the case of `http.batch`. Even with that there is at least one test that previously couldn't have happened with http.batch. closes #2825
- Loading branch information
Showing
4 changed files
with
309 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,235 @@ | ||
package http | ||
|
||
import ( | ||
"testing" | ||
"time" | ||
|
||
"github.com/sirupsen/logrus" | ||
logtest "github.com/sirupsen/logrus/hooks/test" | ||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/require" | ||
"go.k6.io/k6/js/modulestest" | ||
) | ||
|
||
func wrapInAsyncLambda(input string) string { | ||
// This makes it possible to use `await` freely on the "top" level | ||
return "(async () => {\n " + input + "\n })()" | ||
} | ||
|
||
func runOnEventLoop(runtime *modulestest.Runtime, code string) error { | ||
err := runtime.EventLoop.Start(func() error { | ||
_, err := runtime.VU.Runtime().RunString(wrapInAsyncLambda(code)) | ||
return err | ||
}) | ||
runtime.EventLoop.WaitOnRegistered() | ||
return err | ||
} | ||
|
||
func TestAsyncRequest(t *testing.T) { | ||
t.Parallel() | ||
ts := newTestCase(t) | ||
|
||
sr := func(input string) string { | ||
return ts.tb.Replacer.Replace(wrapInAsyncLambda(input)) | ||
} | ||
t.Run("HTTPRequest", func(t *testing.T) { | ||
t.Run("EmptyBody", func(t *testing.T) { | ||
_, err := ts.runtime.VU.Runtime().RunString(sr(` | ||
var reqUrl = "HTTPBIN_URL/cookies" | ||
var res = http.get(reqUrl); | ||
var jar = new http.CookieJar(); | ||
jar.set("HTTPBIN_URL/cookies", "key", "value"); | ||
res = await http.asyncRequest("GET", "HTTPBIN_URL/cookies", null, { cookies: { key2: "value2" }, jar: jar }); | ||
if (res.json().key != "value") { throw new Error("wrong cookie value: " + res.json().key); } | ||
if (res.status != 200) { throw new Error("wrong status: " + res.status); } | ||
if (res.request["method"] !== "GET") { throw new Error("http request method was not \"GET\": " + JSON.stringify(res.request)) } | ||
if (res.request["body"].length != 0) { throw new Error("http request body was not null: " + JSON.stringify(res.request["body"])) } | ||
if (res.request["url"] != reqUrl) { | ||
throw new Error("wrong http request url: " + JSON.stringify(res.request)) | ||
} | ||
if (res.request["cookies"]["key2"][0].name != "key2") { throw new Error("wrong http request cookies: " + JSON.stringify(JSON.stringify(res.request["cookies"]["key2"]))) } | ||
if (res.request["headers"]["User-Agent"][0] != "TestUserAgent") { throw new Error("wrong http request headers: " + JSON.stringify(res.request)) } | ||
`)) | ||
assert.NoError(t, err) | ||
}) | ||
t.Run("NonEmptyBody", func(t *testing.T) { | ||
_, err := ts.runtime.VU.Runtime().RunString(sr(` | ||
var res = await http.asyncRequest("HTTPBIN_URL/post", {a: "a", b: 2}, {headers: {"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"}}); | ||
if (res.status != 200) { throw new Error("wrong status: " + res.status); } | ||
if (res.request["body"] != "a=a&b=2") { throw new Error("http request body was not set properly: " + JSON.stringify(res.request))} | ||
`)) | ||
assert.NoError(t, err) | ||
}) | ||
}) | ||
} | ||
|
||
func TestAsyncRequestResponseCallbackRace(t *testing.T) { | ||
// This test is here only to tease out race conditions | ||
t.Parallel() | ||
ts := newTestCase(t) | ||
ts.runtime.VU.Runtime().Set("q", func(f func()) { | ||
rg := ts.runtime.EventLoop.RegisterCallback() | ||
time.AfterFunc(time.Millisecond*5, func() { | ||
rg(func() error { | ||
f() | ||
return nil | ||
}) | ||
}) | ||
}) | ||
ts.runtime.VU.Runtime().Set("log", func(s string) { | ||
// t.Log(s) // uncomment for debugging | ||
}) | ||
err := runOnEventLoop(ts.runtime, ts.tb.Replacer.Replace(` | ||
let call = (i) => { | ||
log("s"+i) | ||
if (i > 200) { return null; } | ||
http.setResponseCallback(http.expectedStatuses(i)) | ||
q(() => call(i+1)) // don't use promises as they resolve before eventloop callbacks such as the one from asyncRequest | ||
} | ||
for (let j = 0; j< 50; j++) { | ||
call(0) | ||
await http.asyncRequest("GET", "HTTPBIN_URL/redirect/20").then(() => log("!!!!!!!!!!!!!!!"+j)) | ||
} | ||
`)) | ||
require.NoError(t, err) | ||
} | ||
|
||
func TestAsyncRequestErrors(t *testing.T) { | ||
// This likely should have a way to do the same for http.request and http.asyncRequest with the same tests | ||
t.Parallel() | ||
t.Run("Invalid", func(t *testing.T) { | ||
ts := newTestCase(t) | ||
state := ts.runtime.VU.State() | ||
|
||
hook := logtest.NewLocal(state.Logger) | ||
defer hook.Reset() | ||
|
||
err := runOnEventLoop(ts.runtime, `await http.asyncRequest("", "");`) | ||
require.Error(t, err) | ||
assert.Contains(t, err.Error(), "unsupported protocol scheme") | ||
|
||
logEntry := hook.LastEntry() | ||
assert.Nil(t, logEntry) | ||
|
||
t.Run("throw=false", func(t *testing.T) { | ||
hook := logtest.NewLocal(state.Logger) | ||
defer hook.Reset() | ||
|
||
err := runOnEventLoop(ts.runtime, ` | ||
var res = await http.asyncRequest("GET", "some://example.com", null, { throw: false }); | ||
if (res.error.search('unsupported protocol scheme "some"') == -1) { | ||
throw new Error("wrong error:" + res.error); | ||
} | ||
throw new Error("another error"); | ||
`) | ||
require.ErrorContains(t, err, "another error") | ||
|
||
logEntry := hook.LastEntry() | ||
if assert.NotNil(t, logEntry) { | ||
assert.Equal(t, logrus.WarnLevel, logEntry.Level) | ||
err, ok := logEntry.Data["error"].(error) | ||
require.True(t, ok) | ||
assert.ErrorContains(t, err, "unsupported protocol scheme") | ||
assert.Equal(t, "Request Failed", logEntry.Message) | ||
} | ||
}) | ||
}) | ||
t.Run("InvalidURL", func(t *testing.T) { | ||
t.Parallel() | ||
|
||
expErr := `invalid URL: parse "https:// test.k6.io": invalid character " " in host name` | ||
t.Run("throw=true", func(t *testing.T) { | ||
t.Parallel() | ||
ts := newTestCase(t) | ||
|
||
js := ` | ||
await http.asyncRequest("GET", "https:// test.k6.io"); | ||
throw new Error("whoops!"); // shouldn't be reached | ||
` | ||
err := runOnEventLoop(ts.runtime, js) | ||
require.Error(t, err) | ||
assert.Contains(t, err.Error(), expErr) | ||
}) | ||
|
||
t.Run("throw=false", func(t *testing.T) { | ||
t.Parallel() | ||
ts := newTestCase(t) | ||
rt := ts.runtime.VU.Runtime() | ||
state := ts.runtime.VU.State() | ||
state.Options.Throw.Bool = false | ||
defer func() { state.Options.Throw.Bool = true }() | ||
|
||
hook := logtest.NewLocal(state.Logger) | ||
defer hook.Reset() | ||
|
||
js := ` | ||
(async function(){ | ||
var r = await http.asyncRequest("GET", "https:// test.k6.io"); | ||
globalThis.ret = {error: r.error, error_code: r.error_code}; | ||
})() | ||
` | ||
err := runOnEventLoop(ts.runtime, js) | ||
require.NoError(t, err) | ||
ret := rt.GlobalObject().Get("ret") | ||
var retobj map[string]interface{} | ||
var ok bool | ||
if retobj, ok = ret.Export().(map[string]interface{}); !ok { | ||
require.Fail(t, "got wrong return object: %#+v", retobj) | ||
} | ||
require.Equal(t, int64(1020), retobj["error_code"]) | ||
require.Equal(t, expErr, retobj["error"]) | ||
|
||
logEntry := hook.LastEntry() | ||
require.NotNil(t, logEntry) | ||
assert.Equal(t, logrus.WarnLevel, logEntry.Level) | ||
err, ok = logEntry.Data["error"].(error) | ||
require.True(t, ok) | ||
assert.ErrorContains(t, err, expErr) | ||
assert.Equal(t, "Request Failed", logEntry.Message) | ||
}) | ||
|
||
t.Run("throw=false,nopanic", func(t *testing.T) { | ||
t.Parallel() | ||
ts := newTestCase(t) | ||
rt := ts.runtime.VU.Runtime() | ||
state := ts.runtime.VU.State() | ||
state.Options.Throw.Bool = false | ||
defer func() { state.Options.Throw.Bool = true }() | ||
|
||
hook := logtest.NewLocal(state.Logger) | ||
defer hook.Reset() | ||
|
||
js := ` | ||
(async function(){ | ||
var r = await http.asyncRequest("GET", "https:// test.k6.io"); | ||
r.html(); | ||
r.json(); | ||
globalThis.ret = r.error_code; // not reached because of json() | ||
})() | ||
` | ||
err := runOnEventLoop(ts.runtime, js) | ||
ret := rt.GlobalObject().Get("ret") | ||
require.Error(t, err) | ||
assert.Nil(t, ret) | ||
assert.Contains(t, err.Error(), "unexpected end of JSON input") | ||
|
||
logEntry := hook.LastEntry() | ||
require.NotNil(t, logEntry) | ||
assert.Equal(t, logrus.WarnLevel, logEntry.Level) | ||
err, ok := logEntry.Data["error"].(error) | ||
require.True(t, ok) | ||
assert.ErrorContains(t, err, expErr) | ||
assert.Equal(t, "Request Failed", logEntry.Message) | ||
}) | ||
}) | ||
|
||
t.Run("Unroutable", func(t *testing.T) { | ||
t.Parallel() | ||
ts := newTestCase(t) | ||
err := runOnEventLoop(ts.runtime, `await http.asyncRequest("GET", "http://sdafsgdhfjg/");`) | ||
assert.Error(t, err) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters