/web

Http Client fork of Dio

Primary LanguageDartMIT LicenseMIT

Language: English

Web (beta)

A powerful Http client for Dart, which supports Interceptors, Global configuration, FormData, Request Cancellation, File downloading, Timeout etc.

This project is a fork of Dio by wendux. Since the original mantainers, were at the moment of this fork, unable to continue supporting it. And because we wanted to support null-safety, and make some refactorings to make easier for other developers. We decided to make our own version.

Get started

Add dependency

dependencies:
  web:
    path: https://gitlab.com/mobiplay/web/-/tree/master/web

Super simple to use

import 'package:web/web.dart';
void getHttp() async {
  try {
    Response response = await Web().get("http://www.google.com");
    print(response);
  } catch (e) {
    print(e);
  }
}

Table of contents

Examples

Performing a GET request:

Response response;
Web web = new Web();
response = await web.get("/test?id=12&name=wendu");
print(response.data.toString());
// Optionally the request above could also be done as
response = await web.get("/test", queryParameters: {"id": 12, "name": "wendu"});
print(response.data.toString());

Performing a POST request:

response = await web.post("/test", data: {"id": 12, "name": "wendu"});

Performing multiple concurrent requests:

response = await Future.wait([web.post("/info"), web.get("/token")]);

Downloading a file:

response = await web.download("https://www.google.com/", "./xx.html");

Get response stream:

Response<ResponseBody> rs = await Web().get<ResponseBody>(url,
 options: Options(responseType: ResponseType.stream), // set responseType to `stream`
);
print(rs.data.stream); //response stream

Get response with bytes:

Response<List<int>> rs = await Web().get<List<int>>(url,
 options: Options(responseType: ResponseType.bytes), // // set responseType to `bytes`
);
print(rs.data); // List<int>

Sending FormData:

FormData formData = new FormData.fromMap({
    "name": "wendux",
    "age": 25,
  });
response = await web.post("/info", data: formData);

Uploading multiple files to server by FormData:

FormData.fromMap({
    "name": "wendux",
    "age": 25,
    "file": await MultipartFile.fromFile("./text.txt",filename: "upload.txt"),
    "files": [
      await MultipartFile.fromFile("./text1.txt", filename: "text1.txt"),
      await MultipartFile.fromFile("./text2.txt", filename: "text2.txt"),
    ]
});
response = await web.post("/info", data: formData);

Listening the uploading progress:

response = await web.post(
  "http://www.dtworkroom.com/doris/1/2.0.0/test",
  data: {"aa": "bb" * 22},
  onSendProgress: (int sent, int total) {
    print("$sent $total");
  },
);

Post binary data by Stream:

// Binary data
List<int> postData = <int>[...];
await web.post(
  url,
  data: Stream.fromIterable(postData.map((e) => [e])), //create a Stream<List<int>>
  options: Options(
    headers: {
      Headers.contentLengthHeader: postData.length, // set content-length
    },
  ),
);

…you can find all examples code here.

Web APIs

Creating an instance and set default configs.

You can create instance of Web with an optional BaseOptions object:

Web web = new Web(); // with default Options

// Set default configs
web.options.baseUrl = "https://www.xx.com/api";
web.options.connectTimeout = 5000; //5s
web.options.receiveTimeout = 3000;

// or new Web with a BaseOptions instance.
BaseOptions options = new BaseOptions(
    baseUrl: "https://www.xx.com/api",
    connectTimeout: 5000,
    receiveTimeout: 3000,
);
Web web = new Web(options);

The core API in Web instance is:

Future request(String path, {data,Map queryParameters, Options options,CancelToken cancelToken, ProgressCallback onSendProgress, ProgressCallback onReceiveProgress)

response=await request(
    "/test",
    data: {"id":12,"name":"xx"},
    options: Options(method:"GET"),
);

Request method aliases

For convenience aliases have been provided for all supported request methods.

Future get(...)

Future post(...)

Future put(...)

Future delete(...)

Future head(...)

Future put(...)

Future path(...)

Future download(...)

Request Options

The Options class describes the http request information and configuration. Each Web instance has a base config for all requests maked by itself, and we can override the base config with [Options] when make a single request. The [BaseOptions] declaration as follows:

{
  /// Http method.
  String method;

  /// Request base url, it can contain sub path, like: "https://www.google.com/api/".
  String baseUrl;

  /// Http request headers.
  Map<String, dynamic> headers;

   /// Timeout in milliseconds for opening  url.
  int connectTimeout;

   ///  Whenever more than [receiveTimeout] (in milliseconds) passes between two events from response stream,
  ///  [Web] will throw the [Fault] with [FaultType.RECEIVE_TIMEOUT].
  ///  Note: This is not the receiving time limitation.
  int receiveTimeout;

  /// Request data, can be any type.
  T data;

  /// If the `path` starts with "http(s)", the `baseURL` will be ignored, otherwise,
  /// it will be combined and then resolved with the baseUrl.
  String path="";

  /// The request Content-Type. The default value is "application/json; charset=utf-8".
  /// If you want to encode request body with "application/x-www-form-urlencoded",
  /// you can set [Headers.formUrlEncodedContentType], and [Web]
  /// will automatically encode the request body.
  String contentType;

  /// [responseType] indicates the type of data that the server will respond with
  /// options which defined in [ResponseType] are `JSON`, `STREAM`, `PLAIN`.
  ///
  /// The default value is `JSON`, web will parse response string to json object automatically
  /// when the content-type of response is "application/json".
  ///
  /// If you want to receive response data with binary bytes, for example,
  /// downloading a image, use `STREAM`.
  ///
  /// If you want to receive the response data with String, use `PLAIN`.
  ResponseType responseType;

  /// `validateStatus` defines whether the request is successful for a given
  /// HTTP response status code. If `validateStatus` returns `true` ,
  /// the request will be perceived as successful; otherwise, considered as failed.
  ValidateStatus validateStatus;

  /// Custom field that you can retrieve it later in [Interceptor][Transformer] and the   [Response] object.
  Map<String, dynamic> extra;
  
  /// Common query parameters
  Map<String, dynamic /*String|Iterable<String>*/ > queryParameters;  

}

There is a complete example here.

Response Schema

The response for a request contains the following information.

{
  /// Response body. may have been transformed, please refer to [ResponseType].
  T data;
  /// Response headers.
  Headers headers;
  /// The corresponding request info.
  Options request;
  /// Http status code.
  int statusCode;
  /// Whether redirect 
  bool isRedirect;  
  /// redirect info    
  List<RedirectInfo> redirects ;
  /// Returns the final real request uri (maybe redirect). 
  Uri realUri;    
  /// Custom field that you can retrieve it later in `then`.
  Map<String, dynamic> extra;
}

When request is succeed, you will receive the response as follows:

Response response = await web.get("https://www.google.com");
print(response.data);
print(response.headers);
print(response.request);
print(response.statusCode);

Interceptors

For each web instance, We can add one or more interceptors, by which we can intercept requests or responses before they are handled by then or catchError.

web.interceptors.add(InterceptorsWrapper(
    onRequest:(RequestOptions options) async {
     // Do something before request is sent
     return options; //continue
     // If you want to resolve the request with some custom data,
     // you can return a `Response` object or return `web.resolve(data)`.
     // If you want to reject the request with a error message,
     // you can return a `Fault` object or return `web.reject(errMsg)`
    },
    onResponse:(Response response) async {
     // Do something with response data
     return response; // continue
    },
    onError: (Fault e) async {
     // Do something with response error
     return  e;//continue
    }
));

Simple interceptor example:

import 'package:web/web.dart';
class CustomInterceptors extends InterceptorsWrapper {
  @override
  Future onRequest(RequestOptions options) {
    print("REQUEST[${options?.method}] => PATH: ${options?.path}");
    return super.onRequest(options);
  }
  @override
  Future onResponse(Response response) {
    print("RESPONSE[${response?.statusCode}] => PATH: ${response?.request?.path}");
    return super.onResponse(response);
  }
  @override
  Future onError(Fault err) {
    print("ERROR[${err?.response?.statusCode}] => PATH: ${err?.request?.path}");
    return super.onError(err);
  }
}

Resolve and reject the request

In all interceptors, you can interfere with their execution flow. If you want to resolve the request/response with some custom data,you can return a Response object or return web.resolve(data). If you want to reject the request/response with a error message, you can return a Fault object or return web.reject(errMsg) .

web.interceptors.add(InterceptorsWrapper(
  onRequest:(RequestOptions options) {
   return web.resolve("fake data")
  },
));
Response response = await web.get("/test");
print(response.data);//"fake data"

Lock/unlock the interceptors

You can lock/unlock the interceptors by calling their lock()/unlock method. Once the request/response interceptor is locked, the incoming request/response will be added to a queue before they enter the interceptor, they will not be continued until the interceptor is unlocked.

tokenWeb = new Web(); //Create a new instance to request the token.
tokenWeb.options = web;
web.interceptors.add(InterceptorsWrapper(
    onRequest:(Options options) async {
        // If no token, request token firstly and lock this interceptor
        // to prevent other request enter this interceptor.
        web.interceptors.requestLock.lock();
        // We use a new Web(to avoid dead lock) instance to request token.
        Response response = await tokenWeb.get("/token");
        //Set the token to headers
        options.headers["token"] = response.data["data"]["token"];
        web.interceptors.requestLock.unlock();
        return options; //continue
    }
));

You can clean the waiting queue by calling clear();

aliases

When the request interceptor is locked, the incoming request will pause, this is equivalent to we locked the current web instance, Therefore, Web provied the two aliases for the lock/unlock of request interceptors.

web.lock() == web.interceptors.requestLock.lock()

web.unlock() == web.interceptors.requestLock.unlock()

web.clear() == web.interceptors.requestLock.clear()

Example

Because of security reasons, we need all the requests to set up a csrfToken in the header, if csrfToken does not exist, we need to request a csrfToken first, and then perform the network request, because the request csrfToken progress is asynchronous, so we need to execute this async request in request interceptor. The code is as follows:

web.interceptors.add(InterceptorsWrapper(
    onRequest: (Options options) async {
        print('send request:path:${options.path},baseURL:${options.baseUrl}');
        if (csrfToken == null) {
            print("no token,request token firstly...");
            //lock the web.
            web.lock();
            return tokenWeb.get("/token").then((d) {
                options.headers["csrfToken"] = csrfToken = d.data['data']['token'];
                print("request token succeed, value: " + d.data['data']['token']);
                print(
                    'continue to perform request:path:${options.path},baseURL:${options.path}');
                return options;
            }).whenComplete(() => web.unlock()); // unlock the web
        } else {
            options.headers["csrfToken"] = csrfToken;
            return options;
        }
    }
));

For complete codes click here.

Log

You can set LogInterceptor to print request/response log automaticlly, for example:

web.interceptors.add(LogInterceptor(responseBody: false)); //开启请求日志

Custom Interceptor

You can custom interceptor by extending the Interceptor class. There is an example that implementing a simple cache policy: custom cache interceptor.

Cookie Manager

cookie_manager package is a cookie manager for Web.

Handling Errors

When a error occurs, Web will wrap the Error/Exception to a Fault:

try {
    //404
    await web.get("https://wendux.github.io/xsddddd");
} on Fault catch(e) {
    // The request was made and the server responded with a status code
    // that falls out of the range of 2xx and is also not 304.
    if(e.response) {
        print(e.response.data)
        print(e.response.headers)
        print(e.response.request)
    } else{
        // Something happened in setting up or sending the request that triggered an Error
        print(e.request)
        print(e.message)
    }
}

Fault scheme

 {
  /// Response info, it may be `null` if the request can't reach to
  /// the http server, for example, occurring a dns error, network is not available.
  Response response;

  /// Error descriptions.
  String message;

  FaultType type;

  /// The original error/exception object; It's usually not null when `type`
  /// is FaultType.DEFAULT
  dynamic error;
}

FaultType

enum FaultType {
  /// When opening  url timeout, it occurs.
  CONNECT_TIMEOUT,

  ///It occurs when receiving timeout.
  RECEIVE_TIMEOUT,

  /// When the server response, but with a incorrect status, such as 404, 503...
  RESPONSE,

  /// When the request is cancelled, web will throw a error with this type.
  CANCEL,

  /// Default error type, Some other Error. In this case, you can
  /// read the Fault.error if it is not null.
  DEFAULT,
}

Using application/x-www-form-urlencoded format

By default, Web serializes request data(except String type) to JSON. To send data in the application/x-www-form-urlencoded format instead, you can :

//Instance level
web.options.contentType= Headers.formUrlEncodedContentType;
//or works once
web.post("/info", data:{"id":5},
         options: Options(contentType:Headers.formUrlEncodedContentType ));

Sending FormData

You can also send FormData with Web, which will send data in the multipart/form-data, and it supports uploading files.

FormData formData = FormData.fromMap({
    "name": "wendux",
    "age": 25,
    "file": await MultipartFile.fromFile("./text.txt",filename: "upload.txt")
});
response = await web.post("/info", data: formData);

There is a complete example here.

Multiple files upload

There are two ways to add multiple files to FormData, the only difference is that upload keys are different for array types。

  FormData.fromMap({
    "files": [
      MultipartFile.fromFileSync("./example/upload.txt",
          filename: "upload.txt"),
      MultipartFile.fromFileSync("./example/upload.txt",
          filename: "upload.txt"),
    ]
  });

The upload key eventually becomes "files[]",This is because many back-end services add a middle bracket to key when they get an array of files. If you don't want “[]”,you should create FormData as follows(Don't use FormData.fromMap):

  var formData = FormData();
  formData.files.addAll([
    MapEntry(
      "files",
       MultipartFile.fromFileSync("./example/upload.txt",
          filename: "upload.txt"),
    ),
    MapEntry(
      "files",
      MultipartFile.fromFileSync("./example/upload.txt",
          filename: "upload.txt"),
    ),
  ]);

Transformer

Transformer allows changes to the request/response data before it is sent/received to/from the server. This is only applicable for request methods 'PUT', 'POST', and 'PATCH'. Web has already implemented a DefaultTransformer, and as the default Transformer. If you want to customize the transformation of request/response data, you can provide a Transformer by your self, and replace the DefaultTransformer by setting the web.transformer.

In flutter

If you use web in flutter development, you'd better to decode json in background with [compute] function.

// Must be top-level function
_parseAndDecode(String response) {
  return jsonDecode(response);
}

parseJson(String text) {
  return compute(_parseAndDecode, text);
}

void main() {
  ...
  //Custom jsonDecodeCallback
  (web.transformer as DefaultTransformer).jsonDecodeCallback = parseJson;
  runApp(MyApp());
}

Other Example

There is an example for customizing Transformer.

HttpClientAdapter

HttpClientAdapter is a bridge between Web and HttpClient.

Web implements standard and friendly API for developer.

HttpClient: It is the real object that makes Http requests.

We can use any HttpClient not just dart:io:HttpClient to make the Http request. And all we need is providing a HttpClientAdapter. The default HttpClientAdapter for Web is DefaultHttpClientAdapter.

web.httpClientAdapter = new DefaultHttpClientAdapter();

Here is a simple example to custom adapter.

Using proxy

DefaultHttpClientAdapter provide a callback to set proxy to dart:io:HttpClient, for example:

import 'package:web/web.dart';
import 'package:web/adapter.dart';
...
(web.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
    // config the http client
    client.findProxy = (uri) {
        //proxy all request to localhost:8888
        return "PROXY localhost:8888";
    };
    // you can also create a new HttpClient to web
    // return new HttpClient();
};

There is a complete example here.

Https certificate verification

There are two ways to verify the https certificate. Suppose the certificate format is PEM, the code like:

String PEM="XXXXX"; // certificate content
(web.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
    client.badCertificateCallback=(X509Certificate cert, String host, int port){
        if(cert.pem==PEM){ // Verify the certificate
            return true;
        }
        return false;
    };
};

Another way is creating a SecurityContext when create the HttpClient:

(web.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate  = (client) {
    SecurityContext sc = new SecurityContext();
    //file is the path of certificate
    sc.setTrustedCertificates(file);
    HttpClient httpClient = new HttpClient(context: sc);
    return httpClient;
};

In this way, the format of certificate must be PEM or PKCS12.

Http2 support

web_http2_adapter package is a Web HttpClientAdapter which support Http/2.0 .

Cancellation

You can cancel a request using a cancel token. One token can be shared with multiple requests. When a token's cancel method invoked, all requests with this token will be cancelled.

CancelToken token = CancelToken();
web.get(url1, cancelToken: token);
web.get(url2, cancelToken: token);

// cancel the requests with "cancelled" message.
token.cancel("cancelled");

There is a complete example here.

Extends Web class

Web is a abstract class with factory constructor,so we don't extend Web class directy. For this purpose, we can extend WebForNative or WebForBrowser instead, for example:

import 'package:web/web.dart';
import 'package:web/native_imp.dart'; //If in browser, import 'package:web/browser_imp.dart'

class Http extends WebForNative {
  Http([BaseOptions options]):super(options){
    // do something
  }
}

We can also implement our Web client:

class MyWeb with WebMixin implements Web{
  // ...
}

Copyright & License

This open source project authorized by https://tautalos.club , and the license is MIT.

Features and bugs

Please file feature requests and bugs at the issue tracker.

Donate

Buy a cup of coffee for me (Scan by wechat):