Viewing version 2.6.0. Open the latest package page.

w_transport

2.6.0

Platform-agnostic transport library for sending and receiving data over HTTP and WebSocket. HTTP support includes plain-text, JSON, form-data, and multipart data, as well as custom encoding. WebSocket support includes native WebSockets in the browser and the VM with the option to use SockJS in the browser.

Download archive

Readme

w_transport

Pub Build Status codecov.io documentation

Platform-agnostic transport library for sending and receiving data over HTTP and WebSocket. HTTP support includes plain-text, JSON, form-data, and multipart data, as well as custom encoding. WebSocket support includes native WebSockets in the browser and the VM with the option to use SockJS in the browser.

Importing

import 'package:w_transport/w_transport.dart';

This main entry point depends on neither dart:html nor dart:io - it's platform-independent!

With this, you have access to all of our transport classes necessary for sending HTTP requests and establishing WebSocket connections while remaining platform-independent. This means you can use the w_transport library to build components, libraries, or APIs that will be reusable in the browser and on the Dart VM.

The end consumer will make the decision between browser and VM, most likely in a main() block.

Platforms

Browser

import 'package:w_transport/w_transport_browser.dart'
    show configureWTransportForBrowser;

void main() {
  configureWTransportForBrowser();
}

Dart VM

import 'package:w_transport/w_transport_vm.dart'
    show configureWTransportForVM;

void main() {
  configureWTransportForVM();
}

Tests

import 'package:w_transport/w_transport_mock.dart'
    show configureWTransportForTest;

void main() {
  configureWTransportForTest();
}

HTTP

Static Request Methods

For one-off or simple requests, use the static methods on the Http class:

await Http.get(Uri.parse('/ping'));
await Http.post(Uri.parse('/tasks/2'), body: 'new task');

These standard HTTP methods are supported:

  • DELETE : Http.delete()
  • GET : Http.get()
  • HEAD : Http.head()
  • OPTIONS : Http.options()
  • PATCH : Http.patch()
  • POST : Http.post()
  • PUT : Http.put()

If you need to send a request with a non-standard HTTP method, use send():

await Http.send('COPY', Uri.parse('/tasks/4'));

Headers

Map headers = {
  'authorization': 'Bearer sometoken',
  'x-custom': 'value'
};
await Http.get(Uri.parse('/notes/'), headers: headers);

Plain-Text Body

await Http.post(Uri.parse('/notes/'), body: 'testing..');

Plain-text request bodies default to UTF8 encoding.

Request Classes

The above works well for simple requests, but what if you need to send JSON? Or use a different encoding? Send a multi-part request? Or maybe you just want more explicit control over the request. In these cases, you'll want to use one of the exposed request classes:

  • Request
  • FormRequest
  • JsonRequest
  • MultipartRequest
  • StreamedRequest

Every request class extends from BaseRequest.

Common Request API

Though each request type has some idiosyncrasies, they share a common underlying API. We'll cover this common API using Request as the example.

Creating a Request

Request request = new Request();

At this point, there's nothing special about this request, and you'd use it almost exactly like you'd use the top-level request API from above:

Request request = new Request();
await request.post(uri: Uri.parse('/notes/'),
                   headers: {'x-auth-token': 'a390bn'},
                   body: 'A note.');

As you'll notice, all of the parameters are optional - even the uri - because they can be set on the Request directly before being sent:

Request request = new Request()
  ..uri = Uri.parse('/notes/')
  ..headers = {'x-auth-token': 'a390bn'}
  ..body = 'A note.';
await request.post();

For simpler requests, it's recommended that you set the uri, headers, and/or body when dispatching the request (in other words, when you call .get(), .post(), etc).

For requests with more configuration or where the configuration may need to be broken up into several steps, set these properties on the request object directly.

Again, all of the standard HTTP methods are supported and each has its own method (get(), post(), etc). If you need to send a request with a custom HTTP method, use send():

Request request = new Request();
await request.send('COPY', uri: Uri.parse('/notes/6'));

Canceling a Request

All of the request classes support cancellation. At any time, the .abort() method can be called. If the request has not completed yet, it will be canceled. If it has already completed, it has no effect.

Request request = new Request();
request.get(Uri.parse('/notes/'));
...
request.abort();

Credentials (browser only)

HTTP requests made from a browser have an added restriction - secure cookies are not sent by default on cross-origin requests. To include these secure cookies when sending a request, set withCredentials to true. Although this only applies to browsers, it's included in the platform-independent API because it has no effect on the other platforms.

Request request = new Request()
  ..uri = Uri.parse('https://otherhost.com/notes/')
  ..withCredentials = true;
await request.get();

Content-Length, Content-Type and Encoding

All of the request classes set the content-type automatically based on the type of data being sent in the request body, and the charset parameter is set using the encoding's name.

By default, UTF8 encoding is used for requests.

The content-type of a request is available as .contentType and is of type MediaType from the http_parser package.

This property is read-only and is updated based on the type of data in the request body and the encoding. The exception to this is a streamed request where the request body is asynchronous and thus not known ahead of time.

Additionally, the content-length is set automatically for all Request classes since the length of the body in bytes is known before sending. The content-length of a request is available as the read-only property .contentLength and is the number of bytes of the request body when encoded.

Again, the exception to this is StreamedRequest since the body is sent asynchronously.

Consider the following plain-text request:

Request request = new Request()
  ..body = 'Hello World ®';

print(request.contentType.toString());
// content-type: text/plain; charset=utf-8
print(request.contentLength);
// 14

As you can see, the content-type is text/plain because the request body is plain-text and the charset is utf-8 because UTF8 encoding is used by default.

Let's change the encoding:

Request request = new Request()
  ..body = 'Hello World ®'
  ..encoding = LATIN1;

print(request.contentType.toString());
// content-type: text/plain; charset=iso-8859-1
print(request.contentLength);
// 13

The content-type value will change based on the type of request being sent. For plain Requests, it will always be text/plain. The other supported request types will set the content-type as follows:

  • FormRequest: application/x-www-form-urlencoded
  • JsonRequest: application/json
  • MultipartRequest: multipart/form-data

Timeout Threshold

A timeout threshold can be set on any request. If the request takes longer than the set duration, the request will be canceled.

Request request = new Request()
  ..uri = Uri.parse('/notes/')
  ..timeoutThreshold = new Duration(seconds: 15);
request.get();
// This will throw if the request takes longer than 15 seconds.

Request & Response Interception

All of the request classes have hooks that allow request and response interception.

Although hooks for interception are available on the request classes, their purpose is to enable an API on the Client class for intercepting every request created by the client, which is much more useful.

Request interception occurs right before the request is dispatched, at which point changes (async if necessary) to the request instance can be made.

Request request = new Request();

// Register a hook to intercept the request.
request.requestInterceptor = (Request request) async {
  // Modify the request as necessary.
};

Response interception occurs after the response is received but before it is delivered to the caller. At this point, a finalized version of the request can be inspected and the response instance can be modified, augmented, or replaced. Additionally, a RequestException instance will be available if one occurred. Again, this interception can be async if necessary.

Request request = new Request();

// Register a hook to intercept the response.
request.responseInterceptor =
    (FinalizedRequest request, BaseResponse response,
    [RequestException exception]) async {
  // Return a `BaseResponse` instance, modified as necessary.
};

Note that while response interceptors can replace the response instance (and thus are expected to return a BaseResponse instance), request interceptors cannot do this because the request creator's reference would then be incorrect. For this reason, request interceptors must modify the request in place.

Automatic Request Retrying

All of the request classes have an autoRetry API for enabling and configuring automatic request retrying. For example, the following request is configured to automatically retry up to 3 times for non-mutation requests that fail with a 500 or a 502:

Request request = new Request();
request.autoRetry
  ..enabled = true
  ..maxRetries = 3
  ..forHttpMethods = ['GET', 'HEAD', 'OPTIONS']
  ..forStatusCodes = [500, 502];

See the documentation for more information.

Request Types

Now that we've established the API common across all of our Request classes, let's dive into the different types of requests that are supported.

  • JsonRequest
  • FormRequest
  • MultipartRequest
  • Request
  • StreamedRequest

JsonRequest

A JsonRequest sets the content-type to application/json and accepts JSON-encodable Maps or Lists for the request body.

var note = {
  'title': 'My Note',
  'contents': '...',
  'date': new DateTime.now().toString()
};
JsonRequest request = new JsonRequest()
  ..uri = Uri.parse('/notes/')
  ..body = note;
await request.post();

Prior to sending a JsonRequest, the request body will be encoded to an appropriate format (text or bytes, depending on the platform).

FormRequest

A FormRequest sets the content-type to application/x-www-form-urlencoded and accepts a Map<String, String> for the request body where each key-value pair represents a form field's name and value.

By default, a FormRequest's body is an empty Map, allowing you to incrementally set each field.

FormRequest request = new FormRequest()
  ..uri = Uri.parse('/notes/')
  ..fields['title'] = 'My Note'
  ..fields['contents'] = '...'
  ..fields['date'] = new DateTime.now().toString();
await request.post();

MultipartRequest

A MultipartRequest sets the content-type to multipart/form-data and accepts both fields and files for the request body. The MultipartRequest class takes care of generating a unique boundary string used to separate each part of the request body.

The fields are key-value pairs representing a form field's name and value, just like the FormRequest:

MultipartRequest request = new MultipartRequest()
  ..uri = Uri.parse('/notes/')
  ..fields['title'] = 'My Note'
  ..fields['date'] = new DateTime.now().toString();

The files are also key-value pairs, but each pair represents a file's name and object. The actual file object can be several different types.

This is one area where the API is not entirely platform-independent because the APIs for file I/O in the browser are so restricted that they cannot easily be abstracted.

This library includes a MultipartFile class as an option for a platform-independent file abstraction, but it requires that you have access to a byte stream to construct an instance.

The files map accepts the following types:

  • MultipartFile (any platform)
  • dart:html.File (browser)
  • dart:html.Blob (browser)
Stream<List<int>> byteStream;
int length;
MultipartFile file = new MultipartFile(byteStream, length);

MultipartRequest request = new MultipartRequest()
  ..uri = Uri.parse('/notes/')
  ..fields['title'] = 'My Note'
  ..files['attachment'] = file;

Request (plain-text)

A Request sets the content-type to text/plain and accepts either a String or a list of bytes (List<int>) as the body.

// Request body as string
Request request = new Request()
  ..uri = Uri.parse('/notes/')
  ..body = 'My notes.';

// Request body as bytes
Request request = new Request()
  ..uri = Uri.parse('/notes/')
  ..bodyBytes = UTF8.encode('My notes.');

The latter approach is useful if you are already dealing with encoded data - no need to translate back and forth between bytes and text just to fit the API.

Be sure to set encoding if using something other than the default UTF8.

StreamedRequest

A StreamedRequest accepts a byte stream (Stream<List<int>>) as the request body. When a StreamedRequest is sent, the headers will be sent immediately, but the request body will be sent as items are added to the stream. Once the stream has been closed and has finished, the request will end.

List<int> encoded = UTF8.encode('data');
Stream<List<int>> byteStream = new Stream.fromIterable(encoded);

StreamedRequest request = new StreamedRequest()
  ..uri = Uri.parse('/bytes/')
  ..body = byteStream;

Note: the stream you supply should be a single-subscription stream (not a broadcast stream) to avoid losing data.

HTTP Client

An HTTP client acts as a single point from which many requests can be constructed. All requests constructed from a client will inherit headers, the withCredentials flag, and the timeout threshold.

On the server, the Dart VM will also be able to take advantage of cached network connections between requests that share a client.

Client client = new Client()
  ..headers['x-xsrf-token'] = 'ab93c...'
  ..withCredentials = true;

// This request will inherit the above header and withCredentials value.
// Once created, it can be used and dispatched as expected.
Request request = client.newRequest();

If you know that a client will no longer be used, or if you'd like to cancel all outstanding requests from a client, you should close the client. On the server, this ensures that cached network connections are closed.

Client client = new Client();
...
client.close();

Intercepting Requests & Responses from a Client

The request classes have hooks for intercepting the request and the response which the Client class leverages to provide an API for registering a chain of interceptors that will be applied to all requests and subsequent responses created by the client.

class HeaderInterceptor extends HttpInterceptor {
  @override
  Future<RequestPayload> interceptRequest(RequestPayload payload) async {
    payload.request.headers['x-foo'] = 'bar';
    return payload;
  }
}

class QueryParamInterceptor extends HttpInterceptor {
  @override
  Future<RequestPayload> interceptRequest(RequestPayload payload) async {
    payload.request.updateQuery({'baz': 'bar'});
    return payload;
  }
}

main() {
  Client client = new Client()
    ..addInterceptor(new HeaderInterceptor())
    ..addInterceptor(new QueryParamInterceptor());

  // The client will create a request interceptor that chains together the logic
  // from both of the interceptors registered above. This will be set on the
  // request, meaning that this request will have an `x-foo: bar` header and a
  // `baz=bar` query parameter.
  client.newRequest().get(uri: Uri.parse('...'));

Obviously these examples are contrived, but this pattern enables some powerful functionality. Consider the following interceptors as possibilities:

  • Analytics: records types of requests, request duration, request failures, etc.
  • CSRF: sets a header for CSRF verification on outgoing mutation requests and updates said token if the response headers include a new one.
  • OAuth2: sets the Authorization header to a valid OAuth2 token.
  • Session monitoring: watches for failures due to invalid session, like an HTTP 401 failure.

This interceptor logic is asynchronous, which means that you can get really creative. Let's take the CSRF interceptor example and consider a scenario where the initial request requires a CSRF token but one is not known at the time. We can preempt the request and send a separate request to obtain a token:

class CsrfInterceptor extends HttpInterceptor {
  Uri csrfEndpointUri = ...;
  String token;

  @override
  Future<RequestPayload> interceptRequest(RequestPayload payload) async {
    if (token == null) {
      token = await fetchNewToken();
    }
    payload.request.headers['x-xsrf-token'] = token;
    return payload;
  }

  // Assuming we have an endpoint to retrieve a CSRF token.
  Future<String> fetchNewToken() async {
    Response response = await Http.get(csrfEndpointUri);
    return response.body.asJson()['token'];
  }
}

Responses

Every request, once sent, is asynchronous and eventually returns a response. By default, the entire response is loaded into memory and made available to you in three different formats:

Bytes

The response is left in its encoded state and returned directly to you as a list of bytes.

Response response = await Http.get(Uri.parse('/file'));
Uint8List body = response.body.asBytes();

Text

The response's content-type header is inspected for a charset parameter. If found and if valid, the corresponding encoding will be used to decode the response body to text. Otherwise, the default LATIN1 encoding will be used.

Response response = await Http.get(Uri.parse('/file'));
String body = response.body.asString();

JSON

The response is decoded to text (using the above process) and then decoded into either a Map or a List.

This will throw if the response body is not valid JSON.

Response response = await Http.get(Uri.parse('/file'));
Map body = response.body.asJson();

Streamed Responses

As mentioned above, every response is loaded in its entirety into memory by default. This can be problematic for extremely large responses. The solution is to request the response as a stream of data so that it's loaded asynchronously and - if large enough - in chunks.

To request a streamed response, use the corresponding stream method. For example, instead of get(), use streamGet().

StreamedResponse response = await Http.streamGet(Uri.parse('/file'));
response.body.byteStream.listen((List<int> bytes) { ... });

WebSocket

The WebSocket API mirrors the dart:io.WebSocket class to keep things simple. If you've used the VM's WebSocket class before, then you're ready to go. The benefit is that this same API works in the browser as well (even when configured to use SockJS), which is a big improvement over the dart:html.WebSocket class.

The WSocket class included in this library is a Stream and a StreamSink, so sending and receiving data is as simple as adding items to it like a sink and listening to it like a stream.

Establishing a Connection

WSocket webSocket = await WSocket.connect(Uri.parse('ws://echo.websocket.org'));

The connect() method will throw if a connection cannot be established.

Receiving Data

WSocket webSocket = await WSocket.connect(Uri.parse('ws://echo.websocket.org'));

webSocket.listen((data) {
  // Handle message.
}, onError: (error) {
  // Handle error (if desired).
  // The socket will close immediately after this.
}, onDone: () {
  // Perform any cleanup if desired.
});

Sending Data

WSocket webSocket = await WSocket.connect(Uri.parse('ws://echo.websocket.org'));
webSocket.add('message');
await webSocket.addStream(new Stream.fromIterable([...]));

Listening for Completion

WSocket webSocket = await WSocket.connect(Uri.parse('ws://echo.websocket.org'));
webSocket.done.then((_) {
  // Perform cleanup, reopen socket, etc.
}).catchError((error) {
  // Handle socket error, reopen socket, etc.
});

Using SockJS

Sockets can be configured to use SockJS under the hood instead of native WebSockets. This configuration must occur on a per-socket basis.

Uri uri = Uri.parse('ws://echo.websocket.org');
WSocket webSocket = await WSocket.connect(uri,
   useSockJS: true, sockJSProtocolsWhitelist: ['websocket', 'xhr-streaming']);

Testing & Mocks

Just like the browser or the Dart VM, tests are considered a platform for which this library can be configured. By configuring w_transport for tests, mock implementations of all classes will be used.

import 'package:w_transport/w_transport_mock.dart';

main() {
  configureWTransportForTest();
}

That's it. No changes to your source code are necessary! Once configured for test, you are in control of every HTTP request and every WebSocket connection. The APIs for controlling these transports are exported with the w_transport_mock.dart entry point as static APIs on a MockTransports class.

Resetting Mocks:

At any point, you can reset all mock expectations and handlers, giving you a clean state to begin a new mock setup:

MockTransports.reset();

Mocking HTTP

Expecting a request (and returning a 200 OK response by default)

MockTransports.http.expect('GET', Uri.parse('/resource'));

Expecting a request and providing a custom response

Response response = new MockResponse.unauthorized(
    body: 'Invalid access token',
    headers: {'x-session': 'ab93s...'});
MockTransports.http.expect(
    'GET',
    Uri.parse('/resource'),
    respondWith: response);

Expecting a request and causing a failure (exception)

MockTransports.http.expect(
    'GET',
    Uri.parse('/resource'),
    failWith: new Exception('Unexpected error...'));

Registering a handler (expecting multiple requests to the same URI)

MockTransports.http.when(
    Uri.parse('/resource'),
    (FinalizedRequest request) async {
      if (request.method == 'GET') {
        return new MockResponse.ok(body: '...');
      }
      if (request.method == 'DELETE') {
        return new MockResponse(204);
      }
      ...
    });

Registering a handler (expecting multiple requests with the same URI and method)

MockTransports.http.when(
    Uri.parse('/resource'),
    (FinalizedRequest request) async => new MockResponse.ok(body: '...'),
    method: 'GET');

Verifying there are no unresolved requests and/or unsatisfied expectations

MockTransports.verifyNoOutstandingExceptions();

This will throw if an expected request has yet to occur or if a request was made for which no applicable handler was found and was not otherwise expected.

Mocking WebSockets

Expecting and accepting a websocket connection

var ws = new MockWSocket();
MockTransports.webSocket.expect(
    Uri.parse('/ws'),
    connectTo: ws);

Expecting and rejecting a websocket connection

MockTransports.webSocket.expect(Uri.parse('/ws'), reject: true);

This will cause the WSocket.connect(...) clause to throw.

Listening to outgoing data from a mock websocket connection

var ws = new MockWSocket();
ws.onOutgoing((data) { ... });

MockTransports.webSocket.expect(
    Uri.parse('/ws'),
    connectTo: ws);

Sending data to the client from a mock websocket connection

var ws = new MockWSocket();
// Echo.
ws.onOutgoing((data) {
  ws.addIncoming(data);
});

MockTransports.webSocket.expect(
    Uri.parse('/ws'),
    connectTo: ws);

// Create a WSocket instance here...

// Connected message.
ws.add('Connected.');

Triggering a close event for a mock websocket connection

var ws = new MockWSocket();
MockTransports.webSocket.expect(
    Uri.parse('/ws'),
    connectTo: ws);

new Timer(new Duration(seconds: 5), () {
  ws.triggerServerClose();
});

Credits

This library was influenced in many ways by the http package, especially with regard to multipart requests, and served as a useful source for references to pertinent IETF RFCs.

Development

This project leverages the dart_dev package for most of its tooling needs, including static analysis, code formatting, running tests, collecting coverage, and serving examples. Check out the dart_dev readme for more information.

Note: to run integration tests, you'll need two JS dependencies for a SockJS server. Run an npm install to download them.

Changelog

Changelog

2.6.0

June 20, 2016

  • Improvement: The MockTransport utilities now support expecting and registering handlers for HTTP requests and WS connections that match a Pattern instead of exactly matching a URI. Handlers will also receive the Match instance.

    var response = new MockResponse.ok();
    var webSocket = new MockWSocket();
    
    var uriPattern = new RegExp('(http|ws)s:\/\/example.com\/(.*)');
    
    // Capture any GET request to example.com/
    MockTransports.http.expectPattern('GET', uriPattern, respondWith: response);
    
    // Register a handler for any GET request to https://example.com/
    // The `Match` instance will be given to the handler, where it can be used
    // to read any of the captured groups.
    MockTransports.http.whenPattern(uriPattern, (request, match) async {
      print('path: ${match.group(2)}');
      return response;
    }, method: 'GET');
    
    // Capture any WS connection attempt to example.com/
    MockTransports.webSocket.expectPattern(uriPattern, connectTo: webSocket);
    
    // Register a handler for an WS connection attempt to example.com/
    // The `Match` instance will be given to the handler, where it can be used
    // to read any of the captured groups.
    MockTransports.webSocket.whenPattern(uriPattern, handler:
        (uri, {protocols, headers, match}) async {
      print('path: ${match.group(2)}');
      return webSocket;
    });
    
  • Improvement: the content-type for HTTP requests can now be set manually.

    var request = new Request()
      ..uri = Uri.parse('/example')
      ..contentType =
          new MediaType('application', 'x-custom', {'charset': UTF8.name});
    
    • The content-type still has a default value based on the type of request (Request - text/plain, JsonRequest - application/json, etc.).

    • The content-type's charset parameter will still be updated automatically when you set the encoding, but once you manually set contentType, this behavior will stop. In other words, we are assuming that if you set contentType manually, you are intentionally overriding the defaults and are taking responsibility of setting the charset parameter appropriately.

  • Bug Fix: the StreamedRequest now properly verifies that the request has not been sent when setting contentType. It will now throw a StateError like the rest of the request types.

2.5.1

June 16, 2016

  • Error Messaging: When a response body cannot be properly decoded/encoded using the Encoding dictated by the content-type header, a ResponseFormatException will now be thrown with a much more descriptive message. The content-type, encoding, and body will be included.

2.5.0

June 15, 2016

  • Bug Fix: WSocket extends Stream and StreamSink, but was not fulfilling those contracts in all scenarios. In particular:

    • After obtaining a StreamSubscription instance from a call to WSocket.listen(), reassigning the onData(), onError(), and onDone() handlers had no effect.

      var webSocket = await WSocket.connect(...);
      var subscription = webSocket.listen((data) { ... });
      
      // This does nothing:
      subscription.onData((data) { ... });
      // Same goes for onError() and onDone()
      
    • A subscription to a WSocket instance did not properly respect pause and resume signals. This could produce a memory leak by buffering WebSocket events indefinitely.

    • A WSocket instance was immediately listening to the underlying WebSocket and buffering events from the underlying WebSocket until a listener was registered. This is not how a standard Dart Stream works.

    • The SockJS configuration was not properly handling the fact that the SockJS Client produces WebSocket events with a broadcast stream.

    • All of these issues have been addressed, and every WSocket instance should now behave exactly as a standard Stream and StreamSink would, regardless of the platform (VM, browser, SockJS, or mock).

The WSocketCloseEvent class has been deprecated. This class was only used internally and should not have been exported as a part of the public API.

The WSocket implementations are no longer registering an onError handler for the underlying WebSocket stream. If an error occurs on the server, it will not add the error to the stream, it will just close the connection. As a result, the MockWSocket.triggerServerError() method has been deprecated - use MockWSocket.triggerServerClose() instead.

2.4.0

May 4, 2016

  • Improvement: FormRequest now supports fields with multiple values.

    var request = new FormRequest()
      ..fields['multi'] = ['one', 'two'];
    
  • SDK Compatibility: Dart 1.16 exposed a new Client class from the dart:html library that conflicted with the Client class in this library. This has been fixed by adjusting our imports internally, but it may still affect consumers of this library.

  • Documentation: fixed inaccurate documentation around mocking & testing with WebSockets.

2.3.2

March 2, 2016

  • Bug Fix: requests created from a Client now properly inherit all of the autoRetry configuration. Previously the backOff, forTimeouts, and maxRetries settings were missing.

2.3.0

February 11, 2016

Features

  • Implemented retry back-off to allow fixed or exponential back-off between request retries. By default, there is no back-off.

    // Fixed back-off: 1 second between attempts.
    var request = new Request();
    request.autoRetry
      ..enabled = true
      ..backOff = new RetryBackOff.fixed(new Duration(seconds: 1));
    
    // Exponential back-off: 250ms, 500ms, 1s, 2s, etc (base*2^attempt)
    var request = new Request();
    request.autoRetry
      ..enabled = true
      ..backOff = new RetryBackOff.exponential(new Duration(milliseconds: 125));
    
  • Added methods to all request classes for manually retrying failures. This is mainly useful for corner cases where the request's success is dependent on something else and where automatic retrying won't help.

    var request = new Request();
    // send request, catch failure
    
    var response = await request.retry(); // normal
    var response = await request.streamRetry(); // streamed
    
  • Improved error messaging around failed requests. If automatic retrying is enabled, the error message for a failed request will include each individual attempt and why it failed.

2.2.0

February 8, 2016

Features

  • Added an autoRetry.forTimeouts flag (defaults to true) to the Client class and all request classes. This flag determines whether or not requests that are canceled due to exceeding the timeout threshold should be retried.

    // This request will retry if the timeout is exceeded.
    var request = new Request()
      ..timeoutThreshold = new Duration(seconds: 10)
      ..autoRetry.enabled = true;
    
    // This request will NOT retry if the timeout is exceeded.
    var request = new Request()
      ..timeoutThreshold = new Duration(seconds: 10)
      ..autoRetry.enabled = true
      ..autoRetry.forTimeouts = false;
    
  • Added a Duration sockJSTimeout config option to WSocket.connect().

    // browser only
    var socket = await WSocket.connect(Uri.parse('...'),
        useSockJS: true, sockJSTimeout: new Duration(seconds: 5));
    

2.1.0

January 7, 2016

Deprecation: SockJS global configuration

As of v2.0.0, this library could be configured to use SockJS under the hood when the WSocket class was used to establish WebSocket connections. This configuration occurred on a global basis (meaning it affected every WSocket instance) which is undesirable for applications with a mixed usage of native WebSockets and SockJS. This global configuration has been deprecated.

As of v2.1.0, passing useSockJS: true to the configureWTransportForBrowser() method will cause a deprecation warning to be printed to the console.

The SockJS configuration should now occur on a per-socket basis via the WSocket.connect() method:

Uri uri = Uri.parse('ws://echo.websocket.org');
WSocket webSocket = await WSocket.connect(uri,
   useSockJS: true, sockJSProtocolsWhitelist: ['websocket', 'xhr-streaming']);

Features

  • Added a baseUri field to Client that all requests from the client will inherit.

  • All request classes now support a timeout threshold via the timeoutTreshold field. This was also added to the Client class and all requests created from a client will inherit this value.

  • Request and response interception is now supported. This can be done directly on a request instance, but more usefully through a Client instance. See "request & response interception" and "intercepting requests & responses from a client" in the README.

  • All request classes and the Client class now include an API for automatic retrying via the autoRetry field. See "automatic request retrying" in the README.

  • Added a replace method to Response and StreamedResponse to allow simple creation of new responses based on another response, while changing only the fields you specify. This is particularly useful during response interception.

Bug Fixes

  • Headers passed into a request's dispatch method (ex: .get(headers: {...})) are now merged with any existing headers on the request (previously they were being ignored).

2.0.0

November 24, 2015

The 2.0.0 release is a major breaking release. While many of the patterns from 1.0.x were maintained, the HTTP API was broken up into several request classes and two response classes for a much more robust and useful API. As such, there is no backwards compatibility, but a migration guide is included below.

Features

  • WebSockets

    • Single API for the browser and the Dart VM.
    • Option to use SockJS library in place of native WebSockets for the ability to fall back to XHR streaming (configuration only, no API usage difference).
  • HTTP

    • Support for most commonly used request types:
      • Request (content-type: text/plain)
      • JsonRequest (content-type: application/json)
      • FormRequest (content-type: application/x-www-form-urlencoded)
      • MultipartRequest (content-type: multipart/form-data)
    • Synchronous access to response bodies as bytes, text, and JSON.
    • Asynchronous request bodies (StreamedRequest).
    • Asynchronous response bodies via streamGet(), streamPost(), etc. on any of the above request classes.
    • Automatic request encoding and response decoding.
  • Mocks

    • Because this library is designed to be platform-agnostic, it's easy to introduce mocks simply by treating tests as another platform, just like the browser or the Dart VM.
    • Import package:w_transport/w_transport_mock.dart and call configureWTransportForTest() to configure w_transport to use mock implementations for every class.
    • No changes necessary to your source code!
    • Utilize the MockTransports class to control WebSocket connections and HTTP requests.
  • Testing

    • A big initiative in this 2.0.0 release was to increase our test coverage - which we've done. With almost 1000 statements, w_transport has 99.7% coverage!
    • Since this library is concerned with transport protocols, it is imperative that our testing included rigorous integration tests. We have over 1000 integration tests that run in the browser and on the Dart VM against real servers.
    • Our test suites run against our mock implementations as well to ensure they are in parity with the real implementations.

Migration Guide

WRequest

The WRequest class attempted to cover all HTTP request use cases. Its closest analog now is Request - the class for sending plain-text requests. All other request classes share a similar base API with additional support for a specific type of request data (JSON, form, multipart, or streamed).

WResponse

The WResponse class made request meta data (status, headers) available as soon as the request had finished; however, in an attempt to unify the API between the dart:io HTTP requests and dart:html XHR requests, the response body was only available asynchronously (as a stream, an untyped future, or decoded to text). This meant two asynchronous steps were required for every request - one to get the response, and one to get the response body.

This has been greatly improved by switching to two different response classes:

  • Response - response meta data and body available synchronously
  • StreamedResponse - response meta data available synchronously, body available as a stream of bytes

1.0.1

June 23, 2015

Bug Fixes:

  • Allow request data to be set to null.
  • Canceling an in-flight request now properly results in the returned Future completing with an error.
  • Request data type validation now happens when sending the request instead of upon assignment, allowing intermediate data assignments.
  • Verify w_transport configuration has been set before constructing a WHttp instance.

1.0.0

May 21, 2015

  • Initial version of w_transport: a fluent-style, platform-agnostic library with ready to use transport classes for sending and receiving data over HTTP.

Installing

export PUB_HOSTED_URL=https://pub.mirror.p-hatan.ir
flutter pub add 'w_transport:2.6.0'
pubspec.yaml
name: w_transport
version: 2.6.0
description: >
  Platform-agnostic transport library for sending and receiving data over HTTP
  and WebSocket. HTTP support includes plain-text, JSON, form-data, and
  multipart data, as well as custom encoding. WebSocket support includes native
  WebSockets in the browser and the VM with the option to use SockJS in the
  browser.
authors:
  - Workiva Client Platform Team <clientplatform@workiva.com>
  - Dustin Lessard <dustin.lessard@workiva.com>
  - Evan Weible <evan.weible@workiva.com>
  - Jay Udey <jay.udey@workiva.com>
  - Max Peterson <maxwell.peterson@workiva.com>
  - Trent Grover <trent.grover@workiva.com>
homepage: https://github.com/Workiva/w_transport
dependencies:
  fluri: "^1.1.0"
  http_parser: "^1.0.0"
  mime: "^0.9.3"
  sockjs_client:
    git:
      url: https://github.com/Workiva/sockjs-dart-client.git
      ref: 0.3.1
  stack_trace: "^1.4.0"
dev_dependencies:
  ansicolor: "^0.0.9"
  args: "^0.13.0"
  browser: "^0.10.0+2"
  coverage: "^0.7.4"
  dart_dev: "^1.0.0"
  dart_style: "^0.2.1"
  dartdoc: ">=0.4.0 <0.7.0"
  http_server: "^0.9.5+1"
  mockito: "^0.9.0"
  path: "^1.3.5"
  react: "^0.6.1"
  test: "^0.12.3+5"
  uuid: "^0.5.0"
environment:
  sdk: ">=1.9.0 <2.0.0"

Version history

VersionPublishedArchiveSHA-256
1.0.0Cached · Upstream SHA-256 matchedDownload3222509a…48163f
1.0.1Cached · Upstream SHA-256 matchedDownload61990825…beba17
2.0.0Cached · Upstream SHA-256 matchedDownload9feee6c4…603888
2.0.1Cached · Upstream SHA-256 matchedDownloade4e2b50c…0d9350
2.1.0Cached · Upstream SHA-256 matchedDownload7260654a…884f17
2.2.0Cached · Upstream SHA-256 matchedDownload4ae61525…d65b4c
2.3.0Cached · Upstream SHA-256 matchedDownload583a6dfc…551985
2.3.1Cached · Upstream SHA-256 matchedDownload3967a1b5…c7ac73
2.3.2Cached · Upstream SHA-256 matchedDownload9e27eab6…1f3234
2.4.0Cached · Upstream SHA-256 matchedDownload0f67474e…71d178
2.5.0Cached · Upstream SHA-256 matchedDownloadf6af8865…006367
2.5.1Cached · Upstream SHA-256 matchedDownload9ca77c16…a2d873
2.6.0Cached · Upstream SHA-256 matchedDownload3e9cbd47…f442d7
2.7.0Cached · Upstream SHA-256 matchedDownloada73e13da…699cd0
2.7.1Cached · Upstream SHA-256 matchedDownloadf118b095…ea407d
2.8.0Cached · Upstream SHA-256 matchedDownloadcf838cb2…1fe338
2.9.0Cached · Upstream SHA-256 matchedDownload4d05321f…b2484f
2.9.1Cached · Upstream SHA-256 matchedDownload59b61eb3…515e4b
2.9.2Cached · Upstream SHA-256 matchedDownload06c8d45c…a7cdff
2.9.3Cached · Upstream SHA-256 matchedDownload9895612e…2fa208
2.9.4Cached · Upstream SHA-256 matchedDownload8e091ae4…1a48ff
2.9.5Cached · Upstream SHA-256 matchedDownloada0b73bed…c1e587
2.9.6Cached · Upstream SHA-256 matchedDownload7bd284b3…b046d5
2.9.7Cached · Upstream SHA-256 matchedDownloadc2763f8a…2ddc04
2.9.9Cached · Upstream SHA-256 matchedDownloadd6f9e0fe…219616
2.10.0Cached · Upstream SHA-256 matchedDownloadbcd07a9e…92b057
2.10.1Cached · Upstream SHA-256 matchedDownload81b2d987…70550d
3.0.0Cached · Upstream SHA-256 matchedDownload081648c6…d90eb1
3.0.1Cached · Upstream SHA-256 matchedDownload6493e573…207c7a
3.0.2Cached · Upstream SHA-256 matchedDownload5c1a9b2d…184907
3.0.3Cached · Upstream SHA-256 matchedDownload06be9045…767b5b
3.0.4Cached · Upstream SHA-256 matchedDownloadd296a4cb…ab74cb
3.0.5Cached · Upstream SHA-256 matchedDownloadc9960ce4…8f6935
3.0.6Cached · Upstream SHA-256 matchedDownload45e36550…d27ad5
3.1.0Cached · Upstream SHA-256 matchedDownload7f5441f3…40d147
3.1.1Cached · Upstream SHA-256 matchedDownload07b35161…769311
3.1.2Cached · Upstream SHA-256 matchedDownloadcc86ffd7…8e9c53
3.2.0Cached · Upstream SHA-256 matchedDownloadfe9c0912…88d1ad
3.2.1Cached · Upstream SHA-256 matchedDownload551affe3…352520
3.2.2Cached · Upstream SHA-256 matchedDownload16e30166…8dc7f6
3.2.3Cached · Upstream SHA-256 matchedDownloadb7207793…fdde99
3.2.4Cached · Upstream SHA-256 matchedDownload77bc6424…d59de6
3.2.5Cached · Upstream SHA-256 matchedDownloadeaffc5ae…e20972
3.2.6Cached · Upstream SHA-256 matchedDownload054ad1b0…a72904
3.2.7Cached · Upstream SHA-256 matchedDownload365fb82c…3d17f7
3.2.8Cached · Upstream SHA-256 matchedDownload31aba39a…b85424
4.0.0Cached · Upstream SHA-256 matchedDownload4d1e129d…4f9781
4.0.1Cached · Upstream SHA-256 matchedDownload70a9a000…4635df
4.0.2Cached · Upstream SHA-256 matchedDownload24885a5a…95d3c3
4.0.8Cached · Upstream SHA-256 matchedDownload7d00d5da…575eae
4.0.9Cached · Upstream SHA-256 matchedDownloadddf5432f…956aed
4.1.0Cached · Upstream SHA-256 matchedDownloadc61dbd19…a2bc3b
4.1.3Cached · Upstream SHA-256 matchedDownloade8b7da8e…656a84
4.1.4Cached · Upstream SHA-256 matchedDownload32622c7e…f7076c
4.1.5Cached · Upstream SHA-256 matchedDownloada0f06d2a…18b134
4.1.6Cached · Upstream SHA-256 matchedDownload5845272c…9e7680
5.0.0Cached · Upstream SHA-256 matchedDownload889eddc5…e0c289
5.0.1Cached · Upstream SHA-256 matchedDownload06748571…bba3d5
5.1.0Cached · Upstream SHA-256 matchedDownload0e08fe2b…21792d
5.2.0Cached · Upstream SHA-256 matchedDownload4eefeb2f…670259
5.2.1Cached · Upstream SHA-256 matchedDownload6d17ee45…daff4b
5.2.2Cached · Upstream SHA-256 matchedDownloadba45d754…cd1a86
5.2.3Cached · Upstream SHA-256 matchedDownloadd0877828…08890d
5.3.0Cached · Upstream SHA-256 matchedDownloadc9292b40…4fea1d
5.4.0Cached · Upstream SHA-256 matchedDownload0657bf74…4374cb
5.4.1Cached · Upstream SHA-256 matchedDownload8cab1336…fbd35f
5.4.2Cached · Upstream SHA-256 matchedDownloadac876818…e3df57
5.4.3Cached · Upstream SHA-256 matchedDownloade5458f95…d95828
5.4.4Cached · Upstream SHA-256 matchedDownloadb2624cf0…938cd7

Dependencies

fluri "^1.1.0"

http_parser "^1.0.0"

mime "^0.9.3"

sockjs_client { "git": { "ref": "0.3.1", "url": "https://github.com/Workiva/sockjs-dart-client.git" } }

stack_trace "^1.4.0"