Theme
HTTP requests Preview
There is no fetch and no XMLHttpRequest. Outbound HTTP goes through HttpCli, which is synchronous: the call returns when the response has arrived, so the next line of your script can use it. That is the right shape for a script that has to make a decision.
js
var hc = new HttpCli();
var res = hc.Url("https://api.example.com/notify")
.Header("X-Source", "sftp-cloud-connector")
.Timeout(15)
.Get();
if (res.Ok() && res.StatusCode() === 200) {
Log.Info(res.BodyAsString());
} else {
Log.Error("request failed: " + res.StatusCode() + " " + res.ErrorMsg());
}Every configuration method returns the client, so you can chain them or not, as you prefer:
js
var hc = new HttpCli();
hc.Url("https://api.example.com/notify");
hc.Timeout(15);
var res = hc.Get();Configuring the request
| Method | Effect |
|---|---|
Url(url) | Required. The full URL. Last call wins |
Timeout(seconds) | Request timeout in seconds. Last call wins |
Header(name, value) | Adds a header. Additive, call it as often as you like |
Accept(contentType) | Sets the Accept header. Last call wins |
Bearer(token) | Sets Authorization: Bearer <token> |
BasicAuth(user, password) | Standard HTTP basic authentication |
ApiKey(key) | Sets Authorization: Basic <key>, verbatim, with no encoding |
UserAgent(id) | Overrides the User-Agent. The default is sc-conn |
ReqBody(text) | Sets the request body. Read the warning below first |
FormField(name, value) | Adds a form field. Additive. Sends application/x-www-form-urlencoded |
InsecureSkipVerify() | Accepts any TLS certificate, valid or not |
ApiKey does not do what its name suggests
ApiKey("abc") sends Authorization: Basic abc. It does not base64 encode anything, does not add an X-API-Key header and does not append the key to the URL. Almost no API expects that. Use Bearer() for a bearer token, or Header("X-API-Key", key) for a header based key, and reach for ApiKey() only when you have confirmed the server wants exactly that.
InsecureSkipVerify turns off certificate verification entirely
Any certificate is accepted: expired, self signed, or belonging to somebody else. Anyone able to intercept the connection can read and rewrite it, including whatever credential you attached to the request. Use it only against a host on a network you control, and never when the request carries a secret.
Setting a request body
js
hc.ReqBody(text);The body is sent exactly as you supply it. The bytes in the string are the bytes on the wire: no re-encoding, no re-ordering, no wrapper.
js
var res = hc.Url(endpoint)
.ReqBody(JSON.stringify({
event: EventHandler(),
user: CtxUsername(),
path: CtxRelPath(),
attempt: 1,
retry: false
}))
.Post();The Content-Type it picks
| Situation | Content-Type sent |
|---|---|
You set one with Header("Content-Type", ...) | Yours, exactly. Header names are matched case insensitively |
| You did not, and the body parses as JSON | application/json; charset=utf-8 |
| You did not, and it does not | text/plain; charset=utf-8 |
js
// An explicit type always wins.
hc.Url(endpoint).Header("Content-Type", "application/xml").ReqBody(xml).Post();Form fields are a different body
FormField() builds an application/x-www-form-urlencoded body and is the right tool for an endpoint that expects a form:
js
var res = hc.Url("https://intake.example.com/hook")
.FormField("path", CtxRelPath())
.FormField("user", CtxUsername())
.Timeout(10)
.Post();Setting both form fields and a request body is contradictory. The form fields win, the request body is ignored, and a warning is written to the Connector log so you can see it happened. Pick one.
Connector 1.0 and older mangled request bodies
On earlier builds only a flat JSON object whose values were all strings went out as JSON, and even then its keys were re-ordered alphabetically. Everything else, including any JSON containing a number, a boolean, an array or a nested object, was sent as application/octet-stream with a binary prefix in front of it, and an explicit Content-Type header was overwritten. If you are running an older Connector, update it before sending anything but the simplest body.
Verbs
js
.Get() .Post() .Put() .Patch() .Delete() .Head()
.Do(customVerb)Each performs the request and returns a fresh response object.
js
var res = hc.Url("https://api.example.com/thing").Timeout(30).Do("PURGE");Connector 1.0 and older dropped everything on a HEAD
On earlier builds Head() sent a bare request: headers, bearer tokens and basic credentials were all silently discarded, so a HEAD against an authenticated endpoint came back 401 for no visible reason. Fixed; Head() now carries the same configuration as every other verb.
The response
| Method | Returns |
|---|---|
Ok() | true if the request completed. Check this first |
IsValid() | The same thing. Kept for older scripts; prefer Ok() |
ErrorMsg() | Why it did not complete, or "" |
StatusCode() | The HTTP status, or 0 when the request never completed |
BodyAsString() | The body as text |
BodyAsBytes() | The body as an array of bytes |
BodySaveToFile(path) | Streams the body to a local file. Returns true on success |
ContentType() | The Content-Type the server reported |
ContentLength() | The Content-Length the server reported |
Headers() | An object of every response header |
Encoding() | An array of content transfer encodings, usually empty |
Cookies() | An array of cookie objects |
The body can only be read once
BodyAsString(), BodyAsBytes() and BodySaveToFile() all consume the response stream. The first call returns the content; every call after it returns an empty string, an empty array, or writes an empty file. There is no error and no warning.
js
// BROKEN: the log line is empty, because the body was already consumed.
if (res.BodySaveToFile("/var/tmp/payload.json")) {
Log.Info(res.BodyAsString());
}
// Correct: read once, then use the value as often as you like.
var body = res.BodyAsString();
Log.Info(body);
WriteTextToFile("/var/tmp/payload.json", body);Ok() and StatusCode() answer different questions. Ok() is false when the request never completed at all: DNS failure, refused connection, timeout, TLS rejection. In that case StatusCode() is 0 and ErrorMsg() explains it. A 404 is a completed request, so Ok() is true and the status is 404. Check both.
js
var res = hc.Url(endpoint).Timeout(10).Get();
if (!res.Ok()) {
Log.Error("could not reach " + endpoint + ": " + res.ErrorMsg());
Exit(1);
}
if (res.StatusCode() < 200 || res.StatusCode() >= 300) {
Log.Error(endpoint + " answered " + res.StatusCode() + ": " + res.BodyAsString());
Exit(1);
}Downloading a file
js
var res = hc.Url("https://example.com/reference.csv").Timeout(60).Get();
if (res.Ok() && res.StatusCode() === 200) {
if (!res.BodySaveToFile("/var/staging/reference.csv")) {
Log.Error("could not write the downloaded file");
}
}BodySaveToFile streams, so it is the right choice for anything large. It writes to the Connector host's local disk; to land the content in storage a user can see, follow it with ImportFile.
Timeouts and the data path
The client's Timeout() is separate from the handler's timeout, and the smaller one wins in practice: a 60 second HTTP timeout inside a handler with the default 5000 ms budget means the handler dies first.
An HTTP call in a before handler is a remote service deciding your uploads
If the endpoint slows down, every upload slows down with it. If it stops answering, and the handler is fail closed, uploads stop. Put HTTP calls on after handlers marked Run in the background unless the answer genuinely has to gate the operation, and when it does, set a short Timeout() and decide deliberately what a failure should mean.