remove: deleting response.dart

This commit is contained in:
Patrick Stewart 2024-07-04 22:13:05 -07:00
parent 9c5cc567c2
commit d390168198

View file

@ -1,984 +0,0 @@
import 'dart:io';
import 'response_header_bag.dart';
class Response {
// HTTP status codes
static const int HTTP_CONTINUE = 100;
static const int HTTP_SWITCHING_PROTOCOLS = 101;
static const int HTTP_PROCESSING = 102; // RFC2518
static const int HTTP_EARLY_HINTS = 103; // RFC8297
static const int HTTP_OK = 200;
static const int HTTP_CREATED = 201;
static const int HTTP_ACCEPTED = 202;
static const int HTTP_NON_AUTHORITATIVE_INFORMATION = 203;
static const int HTTP_NO_CONTENT = 204;
static const int HTTP_RESET_CONTENT = 205;
static const int HTTP_PARTIAL_CONTENT = 206;
static const int HTTP_MULTI_STATUS = 207; // RFC4918
static const int HTTP_ALREADY_REPORTED = 208; // RFC5842
static const int HTTP_IM_USED = 226; // RFC3229
static const int HTTP_MULTIPLE_CHOICES = 300;
static const int HTTP_MOVED_PERMANENTLY = 301;
static const int HTTP_FOUND = 302;
static const int HTTP_SEE_OTHER = 303;
static const int HTTP_NOT_MODIFIED = 304;
static const int HTTP_USE_PROXY = 305;
static const int HTTP_RESERVED = 306;
static const int HTTP_TEMPORARY_REDIRECT = 307;
static const int HTTP_PERMANENTLY_REDIRECT = 308; // RFC7238
static const int HTTP_BAD_REQUEST = 400;
static const int HTTP_UNAUTHORIZED = 401;
static const int HTTP_PAYMENT_REQUIRED = 402;
static const int HTTP_FORBIDDEN = 403;
static const int HTTP_NOT_FOUND = 404;
static const int HTTP_METHOD_NOT_ALLOWED = 405;
static const int HTTP_NOT_ACCEPTABLE = 406;
static const int HTTP_PROXY_AUTHENTICATION_REQUIRED = 407;
static const int HTTP_REQUEST_TIMEOUT = 408;
static const int HTTP_CONFLICT = 409;
static const int HTTP_GONE = 410;
static const int HTTP_LENGTH_REQUIRED = 411;
static const int HTTP_PRECONDITION_FAILED = 412;
static const int HTTP_REQUEST_ENTITY_TOO_LARGE = 413;
static const int HTTP_REQUEST_URI_TOO_LONG = 414;
static const int HTTP_UNSUPPORTED_MEDIA_TYPE = 415;
static const int HTTP_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
static const int HTTP_EXPECTATION_FAILED = 417;
static const int HTTP_I_AM_A_TEAPOT = 418; // RFC2324
static const int HTTP_MISDIRECTED_REQUEST = 421; // RFC7540
static const int HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918
static const int HTTP_LOCKED = 423; // RFC4918
static const int HTTP_FAILED_DEPENDENCY = 424; // RFC4918
static const int HTTP_TOO_EARLY = 425; // RFC-ietf-httpbis-replay-04
static const int HTTP_UPGRADE_REQUIRED = 426; // RFC2817
static const int HTTP_PRECONDITION_REQUIRED = 428; // RFC6585
static const int HTTP_TOO_MANY_REQUESTS = 429; // RFC6585
static const int HTTP_REQUEST_HEADER_FIELDS_TOO_LARGE = 431; // RFC6585
static const int HTTP_UNAVAILABLE_FOR_LEGAL_REASONS = 451; // RFC7725
static const int HTTP_INTERNAL_SERVER_ERROR = 500;
static const int HTTP_NOT_IMPLEMENTED = 501;
static const int HTTP_BAD_GATEWAY = 502;
static const int HTTP_SERVICE_UNAVAILABLE = 503;
static const int HTTP_GATEWAY_TIMEOUT = 504;
static const int HTTP_VERSION_NOT_SUPPORTED = 505;
static const int HTTP_VARIANT_ALSO_NEGOTIATES_EXPERIMENTAL = 506; // RFC2295
static const int HTTP_INSUFFICIENT_STORAGE = 507; // RFC4918
static const int HTTP_LOOP_DETECTED = 508; // RFC5842
static const int HTTP_NOT_EXTENDED = 510; // RFC2774
static const int HTTP_NETWORK_AUTHENTICATION_REQUIRED = 511; // RFC6585
/// Status codes translation table.
///
/// The list of codes is complete according to the
/// {@link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml Hypertext Transfer Protocol (HTTP) Status Code Registry}
/// (last updated 2021-10-01).
///
/// Unless otherwise noted, the status code is defined in RFC2616.
static const Map<int, String> statusTexts = {
100: 'Continue',
101: 'Switching Protocols',
102: 'Processing', // RFC2518
103: 'Early Hints',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status', // RFC4918
208: 'Already Reported', // RFC5842
226: 'IM Used', // RFC3229
300: 'Multiple Choices',
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
307: 'Temporary Redirect',
308: 'Permanent Redirect', // RFC7238
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Content Too Large', // RFC-ietf-httpbis-semantics
414: 'URI Too Long',
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
418: "I'm a teapot", // RFC2324
421: 'Misdirected Request', // RFC7540
422: 'Unprocessable Content', // RFC-ietf-httpbis-semantics
423: 'Locked', // RFC4918
424: 'Failed Dependency', // RFC4918
425: 'Too Early', // RFC-ietf-httpbis-replay-04
426: 'Upgrade Required', // RFC2817
428: 'Precondition Required', // RFC6585
429: 'Too Many Requests', // RFC6585
431: 'Request Header Fields Too Large', // RFC6585
451: 'Unavailable For Legal Reasons', // RFC7725
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates', // RFC2295
507: 'Insufficient Storage', // RFC4918
508: 'Loop Detected', // RFC5842
510: 'Not Extended', // RFC2774
511: 'Network Authentication Required', // RFC6585
};
/// HTTP response cache control directives
///
/// @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
static const Map<String, bool> httpResponseCacheControlDirectives = {
'must_revalidate': false,
'no_cache': false,
'no_store': false,
'no_transform': false,
'public': false,
'private': false,
'proxy_revalidate': false,
'max_age': true,
's_maxage': true,
'stale_if_error': true, // RFC5861
'stale_while_revalidate': true, // RFC5861
'immutable': false,
'last_modified': true,
'etag': true,
};
ResponseHeaderBag headers;
String content;
String version;
int statusCode;
String statusText;
String? charset;
Map<String, List<String>> sentHeaders = {};
/// @param int $status The HTTP status code (200 "OK" by default)
///
/// @throws \InvalidArgumentException When the HTTP status code is not valid
Response({String? content = '', int status = HTTP_OK, Map<String, List<String?>>? headers})
: headers = ResponseHeaderBag(headers ?? {}),
content = content ?? '',
version = '1.0',
statusCode = status,
statusText = statusTexts[status] ?? 'unknown status';
/// Returns the Response as an HTTP string.
///
/// The string representation of the Response is the same as the
/// one that will be sent to the client only if the prepare() method
/// has been called before.
///
/// @see prepare()
@override
String toString() {
return 'HTTP/$version $statusCode $statusText\r\n$headers\r\n$content';
}
/// Clones the current Response instance.
Response clone() {
return Response(content: content, status: statusCode, headers: headers.all());
}
/// Prepares the Response before it is sent to the client.
///
/// This method tweaks the Response to ensure that it is
/// compliant with RFC 2616. Most of the changes are based on
/// the Request that is "associated" with this Response.
///
/// @return $this
void prepare(HttpRequest request) {
if (isInformational() || isEmpty()) {
content = '';
headers.remove('Content-Type');
headers.remove('Content-Length');
} else {
if (!headers.containsKey('Content-Type')) {
headers.set('Content-Type', 'text/html; charset=${charset ?? 'UTF-8'}');
} else if (headers.value('Content-Type')!.startsWith('text/') && !headers.value('Content-Type')!.contains('charset')) {
headers.set('Content-Type', '${headers.value('Content-Type')}; charset=${charset ?? 'UTF-8'}');
}
if (headers.containsKey('Transfer-Encoding')) {
headers.remove('Content-Length');
}
if (request.method == 'HEAD') {
var length = headers.value('Content-Length');
content = '';
if (length != null) {
headers.set('Content-Length', length);
}
}
}
if (request.protocolVersion != 'HTTP/1.0') {
version = '1.1';
}
if (version == '1.0' && headers.value('Cache-Control')!.contains('no-cache')) {
headers.set('pragma', 'no-cache');
headers.set('expires', '-1');
}
ensureIEOverSSLCompatibility(request);
if (request.uri.scheme == 'https') {
headers.all('Set-Cookie')['set-cookie']?.forEach((cookie) {
if (cookie.contains('; Secure')) return;
headers.set('Set-Cookie', '$cookie; Secure', false);
});
}
}
/// Sends HTTP headers.
///
/// @param positive-int|null $statusCode The status code to use, override the statusCode property if set and not null
///
/// @return $this
void sendHeaders([int? statusCode]) {
if (HttpHeaders.headersSent) {
return;
}
var informationalResponse = statusCode != null && statusCode >= 100 && statusCode < 200;
headers.allPreserveCaseWithoutCookies().forEach((name, values) {
var previousValues = sentHeaders[name];
if (previousValues != null && previousValues == values) {
return;
}
var replace = name.toLowerCase() == 'content-type';
if (previousValues != null && !previousValues.every(values.contains)) {
HttpHeaders.removeHeader(name);
previousValues = null;
}
var newValues = previousValues == null ? values : values.where((v) => !previousValues!.contains(v)).toList();
for (var value in newValues) {
HttpHeaders.setHeader(name, value, replace: replace, statusCode: statusCode ?? this.statusCode);
}
if (informationalResponse) {
sentHeaders[name] = values;
}
});
if (informationalResponse) {
return;
}
statusCode ??= this.statusCode;
HttpHeaders.setHeader('Status', '$version $statusCode $statusText', replace: true, statusCode: statusCode);
}
/// Sends content for the current web response.
///
/// @return $this
void sendContent() {
print(content);
}
/// Sends HTTP headers and content.
///
/// @param bool $flush Whether output buffers should be flushed
///
/// @return $this
void send([bool flush = true]) {
sendHeaders();
sendContent();
if (flush) {
stdout.flush();
}
}
/// Sets the response content.
///
/// @return $this
void setContent(String? content) {
this.content = content ?? '';
}
/// Gets the current response content.
String getContent() {
return content;
}
/// Sets the HTTP protocol version (1.0 or 1.1).
///
/// @return $this
///
/// @final
void setProtocolVersion(String version) {
this.version = version;
}
/// Gets the HTTP protocol version.
///
/// @final
String getProtocolVersion() {
return version;
}
/// Sets the response status code.
///
/// If the status text is null it will be automatically populated for the known
/// status codes and left empty otherwise.
///
/// @return $this
///
/// @throws \InvalidArgumentException When the HTTP status code is not valid
///
/// @final
void setStatusCode(int code, [String? text]) {
statusCode = code;
if (isInvalid()) {
throw ArgumentError('The HTTP status code "$code" is not valid.');
}
statusText = text ?? statusTexts[code] ?? 'unknown status';
}
/// Retrieves the status code for the current web response.
///
/// @final
int getStatusCode() {
return statusCode;
}
/// Sets the response charset.
///
/// @return $this
///
/// @final
void setCharset(String charset) {
this.charset = charset;
}
/// Retrieves the response charset.
///
/// @final
String? getCharset() {
return charset;
}
/// Returns true if the response may safely be kept in a shared (surrogate) cache.
///
/// Responses marked "private" with an explicit Cache-Control directive are
/// considered uncacheable.
///
/// Responses with neither a freshness lifetime (Expires, max-age) nor cache
/// validator (Last-Modified, ETag) are considered uncacheable because there is
/// no way to tell when or how to remove them from the cache.
///
/// Note that RFC 7231 and RFC 7234 possibly allow for a more permissive implementation,
/// for example "status codes that are defined as cacheable by default [...]
/// can be reused by a cache with heuristic expiration unless otherwise indicated"
/// (https://tools.ietf.org/html/rfc7231#section-6.1)
///
/// @final
bool isCacheable() {
if (![200, 203, 300, 301, 302, 404, 410].contains(statusCode)) {
return false;
}
if (headers.hasCacheControlDirective('no-store') || headers.hasCacheControlDirective('private')) {
return false;
}
return isValidateable() || isFresh();
}
/// Returns true if the response is "fresh".
///
/// Fresh responses may be served from cache without any interaction with the
/// origin. A response is considered fresh when it includes a Cache-Control/max-age
/// indicator or Expires header and the calculated age is less than the freshness lifetime.
///
/// @final
bool isFresh() {
return getTtl() > 0;
}
/// Returns true if the response includes headers that can be used to validate
/// the response with the origin server using a conditional GET request.
///
/// @final
bool isValidateable() {
return headers.value('Last-Modified') != null || headers.value('ETag') != null;
}
/// Marks the response as "private".
///
/// It makes the response ineligible for serving other clients.
///
/// @return $this
///
/// @final
void setPrivate() {
headers.remove('Cache-Control');
headers.set('Cache-Control', 'private');
}
/// Marks the response as "public".
///
/// It makes the response eligible for serving other clients.
///
/// @return $this
///
/// @final
void setPublic() {
headers.remove('Cache-Control');
headers.set('Cache-Control', 'public');
}
/// Marks the response as "immutable".
///
/// @return $this
///
/// @final
void setImmutable(bool immutable) {
if (immutable) {
headers.set('Cache-Control', 'immutable');
} else {
headers.remove('Cache-Control');
}
}
/// Returns true if the response is marked as "immutable".
///
/// @final
bool isImmutable() {
return headers.hasCacheControlDirective('immutable');
}
/// Returns true if the response must be revalidated by shared caches once it has become stale.
///
/// This method indicates that the response must not be served stale by a
/// cache in any circumstance without first revalidating with the origin.
/// When present, the TTL of the response should not be overridden to be
/// greater than the value provided by the origin.
///
/// @final
bool mustRevalidate() {
return headers.hasCacheControlDirective('must-revalidate') || headers.hasCacheControlDirective('proxy-revalidate');
}
/// Returns the Date header as a DateTime instance.
///
/// @throws \RuntimeException When the header is not parseable
///
/// @final
DateTime? getDate() {
var date = headers.value('Date');
if (date == null) return null;
return HttpDate.parse(date);
}
/// Sets the Date header.
///
/// @return $this
///
/// @final
void setDate(DateTime date) {
headers.set('Date', HttpDate.format(date.toUtc()));
}
/// Returns the age of the response in seconds.
///
/// @final
int getAge() {
var age = headers.value('Age');
if (age != null) return int.parse(age);
var date = getDate();
if (date == null) return 0;
return DateTime.now().toUtc().difference(date).inSeconds;
}
/// Marks the response stale by setting the Age header to be equal to the maximum age of the response.
///
/// @return $this
void expire() {
if (isFresh()) {
headers.set('Age', getMaxAge().toString());
headers.remove('Expires');
}
}
/// Returns the value of the Expires header as a DateTime instance.
///
/// @final
DateTime? getExpires() {
var expires = headers.value('Expires');
if (expires == null) return null;
try {
return HttpDate.parse(expires);
} catch (_) {
return DateTime.now().subtract(Duration(days: 2));
}
}
/// Sets the Expires HTTP header with a DateTime instance.
///
/// Passing null as value will remove the header.
///
/// @return $this
///
/// @final
void setExpires(DateTime? date) {
if (date == null) {
headers.remove('Expires');
} else {
headers.set('Expires', HttpDate.format(date.toUtc()));
}
}
/// Returns the number of seconds after the time specified in the response's Date
/// header when the response should no longer be considered fresh.
///
/// First, it checks for a s-maxage directive, then a max-age directive, and then it falls
/// back on an expires header. It returns null when no maximum age can be established.
///
/// @final
int? getMaxAge() {
if (headers.hasCacheControlDirective('s-maxage')) {
return int.parse(headers.getCacheControlDirective('s-maxage'));
}
if (headers.hasCacheControlDirective('max-age')) {
return int.parse(headers.getCacheControlDirective('max-age'));
}
var expires = getExpires();
if (expires != null) {
return DateTime.now().difference(expires).inSeconds;
}
return null;
}
/// Sets the number of seconds after which the response should no longer be considered fresh.
///
/// This method sets the Cache-Control max-age directive.
///
/// @return $this
///
/// @final
void setMaxAge(int value) {
headers.set('Cache-Control', 'max-age=$value');
}
/// Sets the number of seconds after which the response should no longer be returned by shared caches when backend is down.
///
/// This method sets the Cache-Control stale-if-error directive.
///
/// @return $this
///
/// @final
void setStaleIfError(int value) {
headers.set('Cache-Control', 'stale-if-error=$value');
}
/// Sets the number of seconds after which the response should no longer return stale content by shared caches.
///
/// This method sets the Cache-Control stale-while-revalidate directive.
///
/// @return $this
///
/// @final
void setStaleWhileRevalidate(int value) {
headers.set('Cache-Control', 'stale-while-revalidate=$value');
}
/// Sets the number of seconds after which the response should no longer be considered fresh by shared caches.
///
/// This method sets the Cache-Control s-maxage directive.
///
/// @return $this
///
/// @final
void setSharedMaxAge(int value) {
setPublic();
headers.set('Cache-Control', 's-maxage=$value');
}
/// Returns the response's time-to-live in seconds.
///
/// It returns null when no freshness information is present in the response.
///
/// When the response's TTL is 0, the response may not be served from cache without first
/// revalidating with the origin.
///
/// @final
int? getTtl() {
var maxAge = getMaxAge();
return maxAge != null ? maxAge - getAge() : null;
}
/// Sets the response's time-to-live for shared caches in seconds.
///
/// This method adjusts the Cache-Control/s-maxage directive.
///
/// @return $this
///
/// @final
void setTtl(int seconds) {
setSharedMaxAge(getAge() + seconds);
}
/// Sets the response's time-to-live for private/client caches in seconds.
///
/// This method adjusts the Cache-Control/max-age directive.
///
/// @return $this
///
/// @final
void setClientTtl(int seconds) {
setMaxAge(getAge() + seconds);
}
/// Returns the Last-Modified HTTP header as a DateTime instance.
///
/// @throws \RuntimeException When the HTTP header is not parseable
///
/// @final
DateTime? getLastModified() {
var lastModified = headers.value('Last-Modified');
if (lastModified == null) return null;
return HttpDate.parse(lastModified);
}
/// Sets the Last-Modified HTTP header with a DateTime instance.
///
/// Passing null as value will remove the header.
///
/// @return $this
///
/// @final
void setLastModified(DateTime? date) {
if (date == null) {
headers.remove('Last-Modified');
} else {
headers.set('Last-Modified', HttpDate.format(date.toUtc()));
}
}
/// Returns the literal value of the ETag HTTP header.
///
/// @final
String? getEtag() {
return headers.value('ETag');
}
/// Sets the ETag value.
///
/// @param string|null $etag The ETag unique identifier or null to remove the header
/// @param bool $weak Whether you want a weak ETag or not
///
/// @return $this
///
/// @final
void setEtag(String? etag, {bool weak = false}) {
if (etag == null) {
headers.remove('ETag');
} else {
if (!etag.startsWith('"')) {
etag = '"$etag"';
}
headers.set('ETag', '${weak ? 'W/' : ''}$etag');
}
}
/// Sets the response's cache headers (validation and/or expiration).
///
/// Available options are: must_revalidate, no_cache, no_store, no_transform, public, private, proxy_revalidate, max_age, s_maxage, immutable, last_modified and etag.
///
/// @return $this
///
/// @throws \InvalidArgumentException
///
/// @final
void setCache(Map<String, dynamic> options) {
var diff = options.keys.toSet().difference(httpResponseCacheControlDirectives.keys.toSet());
if (diff.isNotEmpty) {
throw ArgumentError('Response does not support the following options: "${diff.join(', ')}".');
}
if (options.containsKey('etag')) {
setEtag(options['etag']);
}
if (options.containsKey('last_modified')) {
setLastModified(options['last_modified']);
}
if (options.containsKey('max_age')) {
setMaxAge(options['max_age']);
}
if (options.containsKey('s_maxage')) {
setSharedMaxAge(options['s_maxage']);
}
if (options.containsKey('stale_while_revalidate')) {
setStaleWhileRevalidate(options['stale_while_revalidate']);
}
if (options.containsKey('stale_if_error')) {
setStaleIfError(options['stale_if_error']);
}
httpResponseCacheControlDirectives.forEach((directive, hasValue) {
if (!hasValue && options.containsKey(directive)) {
if (options[directive]) {
headers.set('Cache-Control', directive.replaceAll('_', '-'));
} else {
headers.remove(directive.replaceAll('_', '-'));
}
}
});
if (options.containsKey('public')) {
if (options['public']) {
setPublic();
} else {
setPrivate();
}
}
if (options.containsKey('private')) {
if (options['private']) {
setPrivate();
} else {
setPublic();
}
}
}
/// Modifies the response so that it conforms to the rules defined for a 304 status code.
///
/// This sets the status, removes the body, and discards any headers
/// that MUST NOT be included in 304 responses.
///
/// @return $this
void setNotModified() {
setStatusCode(304);
content = '';
['Allow', 'Content-Encoding', 'Content-Language', 'Content-Length', 'Content-MD5', 'Content-Type', 'Last-Modified'].forEach(headers.remove);
}
/// Returns true if the response includes a Vary header.
///
/// @final
bool hasVary() {
return headers.value('Vary') != null;
}
/// Returns an array of header names given in the Vary header.
///
/// @final
List<String> getVary() {
var vary = headers.all('Vary')['Vary'];
if (vary == null) return [];
return vary.expand((v) => v.split(RegExp(r'[\s,]+'))).toList();
}
/// Sets the Vary header.
///
/// @param bool $replace Whether to replace the actual value or not (true by default)
///
/// @return $this
///
/// @final
void setVary(List<String> headers, [bool replace = true]) {
this.headers.set('Vary', headers.join(', '), replace: replace);
}
/// Determines if the Response validators (ETag, Last-Modified) match
/// a conditional value specified in the Request.
///
/// If the Response is not modified, it sets the status code to 304 and
/// removes the actual content by calling the setNotModified() method.
///
/// @final
bool isNotModified(HttpRequest request) {
if (!['GET', 'HEAD'].contains(request.method)) {
return false;
}
var notModified = false;
var lastModified = headers.value('Last-Modified');
var modifiedSince = request.headers.value(HttpHeaders.ifModifiedSinceHeader);
var ifNoneMatchEtags = request.headers[HttpHeaders.ifNoneMatchHeader];
var etag = getEtag();
if (ifNoneMatchEtags != null && etag != null) {
if (etag.startsWith('W/')) {
etag = etag.substring(2);
}
for (var ifNoneMatchEtag in ifNoneMatchEtags) {
if (ifNoneMatchEtag.startsWith('W/')) {
ifNoneMatchEtag = ifNoneMatchEtag.substring(2);
}
if (ifNoneMatchEtag == etag || ifNoneMatchEtag == '*') {
notModified = true;
break;
}
}
} else if (modifiedSince != null && lastModified != null) {
notModified = DateTime.parse(modifiedSince).isAfter(DateTime.parse(lastModified));
}
if (notModified) {
setNotModified();
}
return notModified;
}
/// Is response invalid?
///
/// @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
///
/// @final
bool isInvalid() {
return statusCode < 100 || statusCode >= 600;
}
/// Is response informative?
///
/// @final
bool isInformational() {
return statusCode >= 100 && statusCode < 200;
}
/// Is response successful?
///
/// @final
bool isSuccessful() {
return statusCode >= 200 && statusCode < 300;
}
/// Is the response a redirect?
///
/// @final
bool isRedirection() {
return statusCode >= 300 && statusCode < 400;
}
/// Is there a client error?
///
/// @final
bool isClientError() {
return statusCode >= 400 && statusCode < 500;
}
/// Was there a server side error?
///
/// @final
bool isServerError() {
return statusCode >= 500 && statusCode < 600;
}
/// Is the response OK?
///
/// @final
bool isOk() {
return statusCode == HTTP_OK;
}
/// Is the response forbidden?
///
/// @final
bool isForbidden() {
return statusCode == HTTP_FORBIDDEN;
}
/// Is the response a not found error?
///
/// @final
bool isNotFound() {
return statusCode == HTTP_NOT_FOUND;
}
/// Is the response a redirect of some form?
///
/// @final
bool isRedirect([String? location]) {
return [HTTP_CREATED, HTTP_MOVED_PERMANENTLY, HTTP_FOUND, HTTP_SEE_OTHER, HTTP_TEMPORARY_REDIRECT, HTTP_PERMANENTLY_REDIRECT].contains(statusCode) &&
(location == null || location == headers.value('Location'));
}
/// Is the response empty?
///
/// @final
bool isEmpty() {
return [HTTP_NO_CONTENT, HTTP_NOT_MODIFIED].contains(statusCode);
}
/// Cleans or flushes output buffers up to target level.
///
/// Resulting level can be greater than target level if a non-removable buffer has been encountered.
///
/// @final
static void closeOutputBuffers(int targetLevel, bool flush) {
while (stdout.hasTerminal && stdout.terminalOutputMode != null && targetLevel > 0) {
if (flush) {
stdout.flush();
} else {
stdout.clear();
}
targetLevel--;
}
}
/// Marks a response as safe according to RFC8674.
///
/// @see https://tools.ietf.org/html/rfc8674
void setContentSafe(bool safe) {
if (safe) {
headers.set('Preference-Applied', 'safe');
} else if (headers.value('Preference-Applied') == 'safe') {
headers.remove('Preference-Applied');
}
setVary(['Prefer'], false);
}
/// Checks if we need to remove Cache-Control for SSL encrypted downloads when using IE < 9.
///
/// @see http://support.microsoft.com/kb/323308
///
/// @final
void ensureIEOverSSLCompatibility(HttpRequest request) {
if (headers.value('Content-Disposition')?.contains('attachment') == true &&
request.headers.value(HttpHeaders.userAgentHeader)?.contains(RegExp(r'MSIE (\d+)')) == true &&
request.uri.scheme == 'https') {
var match = RegExp(r'MSIE (\d+)').firstMatch(request.headers.value(HttpHeaders.userAgentHeader) ?? '');
if (match != null && int.parse(match.group(1)!) < 9) {
headers.remove('Cache-Control');
}
}
}
}