blob: 617cc159fd681eeab27d528932c61fb8d32d02e3 [file] [log] [blame]
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:js_interop';
import 'dart:typed_data';
import '../dom.dart';
import 'events/events.dart' show XHRGetters;
/// A helper used to make it easier to operate over [XMLHttpRequest]s.
///
/// The logic here was copied from `dart:html` to help bridge a functionality
/// gap missing in `package:web`.
///
/// HttpRequest can be used to obtain data from HTTP and FTP protocols,
/// and is useful for AJAX-style page updates.
///
/// The simplest way to get the contents of a text file, such as a
/// JSON-formatted file, is with [getString].
/// For example, the following code gets the contents of a JSON file
/// and prints its length:
///
/// var path = 'myData.json';
/// HttpRequest.getString(path).then((String fileContents) {
/// print(fileContents.length);
/// }).catchError((error) {
/// print(error.toString());
/// });
///
/// ## Fetching data from other servers
///
/// For security reasons, browsers impose restrictions on requests
/// made by embedded apps.
/// With the default behavior of this class,
/// the code making the request must be served from the same origin
/// (domain name, port, and application layer protocol)
/// as the requested resource.
/// In the example above, the myData.json file must be co-located with the
/// app that uses it.
///
/// ## Other resources
///
/// * [Fetch data dynamically](https://dart.dev/tutorials/web/fetch-data/),
/// a tutorial shows how to load data from a static file or from a server.
/// * [Dart article on using HttpRequests](https://dart.dev/guides/libraries/library-tour#using-http-resources-with-httprequest)
/// * [JS XMLHttpRequest](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest)
/// * [Using XMLHttpRequest](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest/Using_XMLHttpRequest)
class HttpRequest {
// The lint is ignored because these are purposely made to match the old
// names used in `dart:html`
// ignore:constant_identifier_names
static const int DONE = 4;
// ignore:constant_identifier_names
static const int HEADERS_RECEIVED = 2;
// ignore:constant_identifier_names
static const int LOADING = 3;
// ignore:constant_identifier_names
static const int OPENED = 1;
// ignore:constant_identifier_names
static const int UNSENT = 0;
/// Creates a GET request for the specified [url].
///
/// This is similar to [request] but specialized for HTTP GET requests which
/// return text content.
///
/// To add query parameters, append them to the [url] following a `?`, joining
/// each key to its value with `=` and separating key-value pairs with `&`.
///
/// var name = Uri.encodeQueryComponent('John');
/// var id = Uri.encodeQueryComponent('42');
/// HttpRequest.getString('users.json?name=$name&id=$id')
/// .then((String resp) {
/// // Do something with the response.
/// });
///
/// See also [request].
static Future<String> getString(String url,
{bool? withCredentials, void Function(ProgressEvent)? onProgress}) =>
request(url, withCredentials: withCredentials, onProgress: onProgress)
.then((r) => r.responseText);
/// Makes a server POST request with the specified data encoded as form data.
///
/// This is roughly the POST equivalent of [getString]. This method is similar
/// to sending a [FormData] object with broader browser support but limited to
/// String values.
///
/// If [data] is supplied, the key/value pairs are URI encoded with
/// [Uri.encodeQueryComponent] and converted into an HTTP query string.
///
/// Unless otherwise specified, this method appends the following header:
///
/// Content-Type: application/x-www-form-urlencoded; charset=UTF-8
///
/// Here's an example of using this method:
///
/// var data = { 'firstName' : 'John', 'lastName' : 'Doe' };
/// HttpRequest.postFormData('/send', data).then((HttpRequest resp) {
/// // Do something with the response.
/// });
///
/// See also [request].
static Future<XMLHttpRequest> postFormData(
String url, Map<String, String> data,
{bool? withCredentials,
String? responseType,
Map<String, String>? requestHeaders,
void Function(ProgressEvent)? onProgress}) {
final parts = <String>[];
data.forEach((key, value) {
parts.add('${Uri.encodeQueryComponent(key)}='
'${Uri.encodeQueryComponent(value)}');
});
final formData = parts.join('&');
requestHeaders ??= <String, String>{};
requestHeaders.putIfAbsent('Content-Type',
() => 'application/x-www-form-urlencoded; charset=UTF-8');
return request(url,
method: 'POST',
withCredentials: withCredentials,
responseType: responseType,
requestHeaders: requestHeaders,
sendData: formData,
onProgress: onProgress);
}
/// Creates and sends a URL request for the specified [url].
///
/// By default `request` will perform an HTTP GET request, but a different
/// method (`POST`, `PUT`, `DELETE`, etc) can be used by specifying the
/// [method] parameter. (See also [HttpRequest.postFormData] for `POST`
/// requests only.
///
/// The Future is completed when the response is available.
///
/// If specified, `sendData` will send data in the form of a [ByteBuffer],
/// [Blob], [Document], [String], or [FormData] along with the HttpRequest.
///
/// If specified, [responseType] sets the desired response format for the
/// request. By default it is [String], but can also be 'arraybuffer', 'blob',
/// 'document', 'json', or 'text'.
/// for more information.
///
/// The [withCredentials] parameter specified that credentials such as a
/// cookie (already) set in the header or
/// [authorization headers](http://tools.ietf.org/html/rfc1945#section-10.2)
/// should be specified for the request. Details to keep in mind when using
/// credentials:
///
/// * Using credentials is only useful for cross-origin requests.
/// * The `Access-Control-Allow-Origin` header of `url` cannot contain a
/// wildcard (*).
/// * The `Access-Control-Allow-Credentials` header of `url` must be set to
/// true.
/// * If `Access-Control-Expose-Headers` has not been set to true, only a
/// subset of all the response headers will be returned when calling
/// `getAllResponseHeaders`.
///
/// The following is equivalent to the [getString] sample above:
///
/// var name = Uri.encodeQueryComponent('John');
/// var id = Uri.encodeQueryComponent('42');
/// HttpRequest.request('users.json?name=$name&id=$id')
/// .then((HttpRequest resp) {
/// // Do something with the response.
/// });
///
/// Here's an example of submitting an entire form with [FormData].
///
/// var myForm = querySelector('form#myForm');
/// var data = new FormData(myForm);
/// HttpRequest.request('/submit', method: 'POST', sendData: data)
/// .then((HttpRequest resp) {
/// // Do something with the response.
/// });
///
/// Note that requests for file:// URIs are only supported by Chrome
/// extensions with appropriate permissions in their manifest. Requests to
/// file:// URIs will also never fail- the Future will always complete
/// successfully, even when the file cannot be found.
///
/// See also: [authorization headers][1].
///
/// [1]: http://en.wikipedia.org/wiki/Basic_access_authentication
static Future<XMLHttpRequest> request(String url,
{String? method,
bool? withCredentials,
String? responseType,
String? mimeType,
Map<String, String>? requestHeaders,
Object? sendData,
void Function(ProgressEvent)? onProgress}) {
final completer = Completer<XMLHttpRequest>();
final xhr = XMLHttpRequest();
method ??= 'GET';
xhr.open(method, url, true);
if (withCredentials != null) {
xhr.withCredentials = withCredentials;
}
if (responseType != null) {
xhr.responseType = responseType;
}
if (mimeType != null) {
xhr.overrideMimeType(mimeType);
}
// ignore: unnecessary_lambdas
requestHeaders?.forEach((a, b) => xhr.setRequestHeader(a, b));
if (onProgress != null) {
xhr.onProgress.listen(onProgress);
}
xhr.onLoad.listen((ProgressEvent e) {
final status = xhr.status;
final accepted = status >= 200 && status < 300;
final fileUri = status == 0; // file:// URIs have status of 0.
final notModified = status == 304;
// Redirect status is specified up to 307, but others have been used in
// practice. Notably Google Drive uses 308 Resume Incomplete for
// resumable uploads, and it's also been used as a redirect. The
// redirect case will be handled by the browser before it gets to us,
// so if we see it we should pass it through to the user.
final unknownRedirect = status > 307 && status < 400;
if (accepted || fileUri || notModified || unknownRedirect) {
completer.complete(xhr);
} else {
completer.completeError(e);
}
});
xhr.onError.listen(completer.completeError);
if (sendData != null) {
xhr.send(sendData is String ? sendData.toJS : sendData.jsify());
} else {
xhr.send();
}
return completer.future;
}
}