HTTP Client
Unisave provides a thin wrapper around the .NET HttpClient
that lets you easily make HTTP requests to external web services.

If you have never communicated with a web service before, you can read the HTTP and the Web explained for game developers guide. The guide is an introduction into the topic, whereas this documentation page serves as a reference documentation for all the available HTTP client features.
Asynchronicity
The HTTP clients provides most methods in both a synchronous (i.e. Get
) and asynchronous variant (i.e. GetAsync
+ the await
keyword). It is much better to use the asynchronous methods, because the server process can work on other requests while waiting for the HTTP client to finish (which can be a few seconds). While synchronous methods might seem easier to use now, you are guaranteed to hit performance issues once your traffic gets to the "requests per second" range.
// ✅ DO
var response = await Http.GetAsync("http://test.com");
// ❌ DON'T
var response = Http.Get("http://test.com");
If you've never seen async
and await
before, go and read the Async-Await for Dummies guide. You don't need to know how asynchronicity works in order to use it. In the end, you just need to add a few keywords in a couple of places.
Making requests
To make HTTP requests, you may use the GetAsync
, PostAsync
, PutAsync
, PatchAsync
and DeleteAsync
methods. An example GET
request is as easy as:
using Unisave.Facades;
var response = await Http.GetAsync("http://test.com");
You can then inspect the response:
// content
response.Body(); // string
response.Json(); // JsonObject
response.Form(); // Dictionary<string, string>
response.Bytes(); // byte[]
response.Stream(); // Stream
// status
response.Status; // int
response.IsOk; // true when status is 200
response.IsSuccessful; // true when status is 2xx
response.Failed; // true when status is 4xx or 5xx
response.IsClientError; // true when status is 4xx
response.IsServerError; // true when status is 5xx
// headers
response.Header("Content-Type"); // string
If the response is application/json
or application/x-www-form-urlencoded
, an indexer access can be used:
// get title of the latest news item for Team Fortress 2
var response = await Http.GetAsync(
"https://api.steampowered.com/" +
"ISteamNews/GetNewsForApp/v2/" +
"?appid=440&count=1"
);
return response["appnews"]["newsitems"][0]["title"];
Request data
POST request with JSON body
It's common to send request body with a POST
request. The most common body type is a JSON object:
using LightJson;
var response = await Http.PostAsync(
"http://test.com/do-something",
new JsonObject {
["foo"] = "bar",
["baz"] = 42
}
);
Info: Methods
PUT
,PATCH
andDELETE
also support this approach.
GET request with query parameters
Instead of appending the query string directly to the URL (authough you still can), you can pass it as a dictionary via the second argument:
var response = await Http.GetAsync(
"http://test.com/query-something",
new Dictionary<string, string> {
["foo"] = "bar",
["baz"] = "42"
}
);
Info: Giving a
Dictionary<string, string>
as a second argument to other methods (POST
,PUT
,PATCH
andDELETE
) will send it as an form URL encoded body.
Specifying request body in advance
You can specify the request body before you send the request:
// JSON
var response = await Http.WithJsonBody(
new JsonObject {
["foo"] = "bar",
["baz"] = 42
}
).PostAsync("http://test.com/do-something");
// Form URL encoded
var response = await Http.WithFormBody(
new Dictionary<string, string> {
["foo"] = "bar",
["baz"] = "42"
}
).PostAsync("http://test.com/do-something");
Sending raw HttpContent
If you want to send a different body, you can always fallback to the .NET HttpContent
class:
using System.Net.Http;
var response = await Http.WithBody(
new StringContent(
"Hello!",
Encoding.UTF8,
"text/plain"
)
).PostAsync("http://test.com/do-something");
Headers
You can add additional headers to the request using the WithHeaders
method:
var response = await Http.WithHeaders(
new Dictionary<string, string>() {
["X-First"] = "foo",
["X-Second"] = "bar"
}
).PostAsync("http://test.com/do-something");
Authentication
You may add authentication information via the HTTP basic authentication scheme or the OAuth 2.0 bearer token scheme:
var response = await Http
.WithBasicAuth("username", "password")
.PostAsync(...);
var response = await Http
.WithToken("token")
.PostAsync(...);
Timeout
You can specify the maximum time to wait for the response. The default timeout is 10 seconds. Once the time runs out, a TaskCancelledException
is thrown.
var response = await Http
.WithTimeout(30.0) // seconds or TimeSpan
.PostAsync(...);
Cancellation
The request can also be cancelled on your terms by providing a CancellationToken
to the request. This works in addition to the timeout above, so for long-running reuqests you might also want to set an infinite timeout. When the cancellation token is triggered, a TaskCancelledException
is thrown.
CancellationToken token; // your token
var response = await Http
.WithCancellation(token)
.PostAsync(...);
Streamed responses
The HTTP client by default finishes after receiving the whole HTTP response and reading it into a buffer. This lets you access the response synchronously without await
:
var response = await Http.GetAsync(
"https://small-response.com"
);
byte[] data = response.Bytes(); // no await needed
If the response is large or streamed and you don't want to buffer it, you can disable the buffering with WithoutResponseBuffering()
and then read the response slowly yourself:
var response = await Http
.WithoutResponseBuffering() // do not read the body yet
.GetAsync(
"https://large-response.com"
);
Stream stream = await response.StreamAsync(); // await here
// read 1 KB from the response
bytes[] buffer = new bytes[1024];
await stream.ReadAsync(buffer, 0, buffer.Length);
Error handling
Unisave HTTP client does not throw exceptions on client or server errors (4xx
or 5xx
status codes).
You can check for the error using the following properties:
response.Status; // int
response.IsOk; // true when status is 200
response.IsSuccessful; // true when status is 2xx
response.Failed; // true when status is 4xx or 5xx
response.IsClientError; // true when status is 4xx
response.IsServerError; // true when status is 5xx
If you, however, do want to throw an exception in such case, you can call the Throw
method. This method will, of course, not do anything if the request was successful.
var response = await Http.PostAsync(...);
response.Throw();
return response["foo"]["bar"];
The method will throw an instance of System.Net.HttpRequestException
.
The method also returns the response itself so it can be chained:
return (await Http.PostAsync(...))
.Throw()
.Json()["foo"]["bar"];
The underlying .NET instance
Whenever you have an instance of a request or a response, you can access the respective HttpRequestMessage
and HttpResponseMessage
via the .Original
property:
var response = await Http.GetAsync(...);
response.Original.StatusCode; // 404
response.Original.ReasonPhrase; // "Not found"
Concurrent requests
Sometimes you may want to make multiple requests at the same time to multiple places. You can do this like with any other C# Task
s, by starting a number of them and then awaiting them later:
// vvv-- notice missing "await"
Task<Response> firstTask = Http.GetAsync("https://first.com");
var secondTask = Http.GetAsync("https://second.com");
var thirdTask = Http.GetAsync("https://third.com");
// all requests have been sent,
// now we can wait for their responses
Response firstResponse = await firstTask;
var secondResponse = await secondTask;
var thirdResponse = await thirdTask;
// now you can work with those responses
Dependency injection
All the examples in this documentation use the Http
static class facade. You can also access the same API via an injected IHttp
service from the Unisave service container. This is an example usecase in a facet:
using Unisave.Facets;
using Unisave.HttpClient;
class MyFacet : Facet
{
private IHttp http;
// ask the service container about the service here
public MyFacet(IHttp http)
{
this.http = http;
}
public async Task MakeSomeReuqest()
{
var response = await http.GetAsync(
"https://test.com"
);
// ...
}
}
Testing
This section is related to automated testing of your backend. It is intended for tests where the backend code is executed locally as part of the test suite, not on the server.
The Http.Fake(...)
method allows you to return dummy responses when requests are made.
Faking responses
For example you can return an empty 200
status code response for every request made just by calling the Fake
method without any arguments:
Http.Fake();
var response = Http.Post(...);
You can also fake only a specific URL and specify the response to be returned:
Http.Fake("github.com/*", Http.Response(new JsonObject {
["foo"] = "bar"
}, 200));
Http.Fake(
"google.com/*",
Http.Response("Hello!", "text/plain")
);
You can also fake response headers:
Http.Fake("google.com/*",
Http.Response(
new JsonObject(),
200,
new Dictionary<string, string> {
["X-Header"] = "value"
}
)
);
Again, if you omit the URL address, it will fake all requests:
Http.Fake(
Http.Response(...)
);
Response sequences
Sometimes you want to return a sequence of responses from a URL. You can use the Http.Sequence()
method for that:
Http.Fake(
Http.Sequence()
.Push("Hello!", "text/plain", 200)
.Push(new JsonObject {...}, 200, headers)
.PushStatus(404)
);
When all the responses have been consumed, any furter requests will throw an exception. You may, however, specify a default response that should be returned when the sequence is empty, you may use the WhenEmpty
method:
Http.Fake(
Http.Sequence()
.Push(...)
.Push(...)
.WhenEmpty(Http.Response(...))
);
But if you want to return just a plain 200
empty response, you can simplify it to:
Http.Fake(
Http.Sequence()
.Push(...)
.Push(...)
.DontFailWhenEmpty()
);
Fake callback
If you require more complicated logic to determine what responses to return, you may pass a callback to the faking method. This callback will receive an instance of Unisave.Http.Client.Request
and should return a response instance.
Http.Fake("test.com/*"
request => {
string name = request["name"];
return Http.Response($"Hello {name}!");
}
);
Note: If the callback returns
null
, it will be ignored and the next callback in the row will be called. This continues until there are no more callbacks and then the request gets sent for real. All the faking methods described above actually just register callbacks under the hood.
Inspecting requests
When faking responses, you may sometimes want to inspect the requests that have been made in order to check that your backend is sending the correct data or headers. You may acomplish this by calling the Http.AssertSent
method after calling Http.Fake
.
The AssertSent
method accepts a callback which will be given an instance of a Unisave.Http.Client.Request
and it should return a boolean value indicating if the request matches your expectations. In order for the test to pass, at least one request must have been issued matching the given expectations:
Http.Fake();
Http.WithHeaders(new Dictionary<string, string> {
["X-First"] = "foo"
}).Post("http://test.com/players", new JsonObject {
["name"] = "Bob",
["coins"] = 123
});
Http.AssertSent(request =>
request.HasHeader("X-First", "foo") &&
request.Url == "https://test.com/players" &&
request["name"] == "Bob" &&
request["coins"] == 123
);
You may also assert that a specific request was not sent:
Http.AssertNotSent(request =>
request.Url == "http://test.com/foo"
);
Or you might want to check that no requests were sent at all:
Http.AssertNothingSent();