Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/postgrest/lib/src/postgrest.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,15 @@ class PostgrestClient {
final bool _hasCustomIsolate;
final bool retryEnabled;
final Duration Function(int attempt)? _retryDelay;

/// Optional timeout in milliseconds for PostgREST requests. When set,
/// requests automatically abort after this duration to prevent indefinite hangs.
final int? timeout;

/// Maximum URL length in characters before a warning is logged. Defaults to 8000.
/// Protects against exceeding server URL limits with large queries.
final int urlLengthLimit;

final _log = Logger('supabase.postgrest');

/// To create a [PostgrestClient], you need to provide an [url] endpoint.
Expand All @@ -32,6 +41,10 @@ class PostgrestClient {
/// [retryEnabled] controls whether automatic retries are performed for GET and
/// HEAD requests that fail with HTTP 503, HTTP 520, or a network error. Defaults to `true`.
/// Use [PostgrestBuilder.retry] to override this per request.
///
/// [timeout] is optional and can be used to set a timeout in milliseconds for requests
///
/// [urlLengthLimit] is optional and can be used to set the maximum URL length before a warning is logged. Defaults to 8000.
PostgrestClient(
this.url, {
Map<String, String>? headers,
Expand All @@ -40,6 +53,8 @@ class PostgrestClient {
YAJsonIsolate? isolate,
this.retryEnabled = true,
@visibleForTesting Duration Function(int attempt)? retryDelay,
this.timeout,
this.urlLengthLimit = 8000,
}) : _schema = schema,
headers = {...defaultHeaders, if (headers != null) ...headers},
_isolate = isolate ?? (YAJsonIsolate()..initialize()),
Expand Down Expand Up @@ -77,6 +92,8 @@ class PostgrestClient {
isolate: _isolate,
retryEnabled: retryEnabled,
retryDelay: _retryDelay,
timeout: timeout,
urlLengthLimit: urlLengthLimit,
);
}

Expand All @@ -92,6 +109,8 @@ class PostgrestClient {
isolate: _isolate,
retryEnabled: retryEnabled,
retryDelay: _retryDelay,
timeout: timeout,
urlLengthLimit: urlLengthLimit,
);
}

Expand Down Expand Up @@ -124,6 +143,8 @@ class PostgrestClient {
isolate: _isolate,
retryEnabled: retryEnabled,
retryDelay: _retryDelay,
timeout: timeout,
urlLengthLimit: urlLengthLimit,
).rpc(params, get);
}

Expand Down
73 changes: 54 additions & 19 deletions packages/postgrest/lib/src/postgrest_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
final CountOption? _count;
final bool _retryEnabled;
final Duration Function(int attempt) _retryDelay;

/// Optional timeout in milliseconds for this request. When set, the request
/// automatically aborts after this duration to prevent indefinite hangs.
final int? _timeout;

/// Maximum URL length in characters before a warning is logged. Defaults to 8000.
final int _urlLengthLimit;

final _log = Logger('supabase.postgrest');

static Duration _defaultRetryDelay(int attempt) =>
Expand All @@ -69,6 +77,8 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
PostgrestConverter<S, R>? converter,
bool retryEnabled = true,
@visibleForTesting Duration Function(int attempt)? retryDelay,
int? timeout,
int urlLengthLimit = 8000,
}) : _maybeSingle = maybeSingle,
_method = method,
_converter = converter,
Expand All @@ -80,7 +90,9 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
_count = count,
_body = body,
_retryEnabled = retryEnabled,
_retryDelay = retryDelay ?? _defaultRetryDelay;
_retryDelay = retryDelay ?? _defaultRetryDelay,
_timeout = timeout,
_urlLengthLimit = urlLengthLimit;

PostgrestBuilder<T, S, R> _copyWith({
Uri? url,
Expand Down Expand Up @@ -109,6 +121,8 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
converter: converter ?? _converter,
retryEnabled: retryEnabled ?? _retryEnabled,
retryDelay: retryDelay ?? _retryDelay,
timeout: _timeout,
urlLengthLimit: _urlLengthLimit,
);
}

Expand All @@ -134,28 +148,39 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
// X-Retry-Count, etc.).
final execHeaders = {..._headers};

if (_count != null) {
final count = _count;
if (count != null) {
if (execHeaders['Prefer'] != null) {
final oldPreferHeader = execHeaders['Prefer'];
execHeaders['Prefer'] = '$oldPreferHeader,count=${_count.name}';
execHeaders['Prefer'] = '$oldPreferHeader,count=${count.name}';
} else {
execHeaders['Prefer'] = 'count=${_count.name}';
execHeaders['Prefer'] = 'count=${count.name}';
}
}

final urlLength = _url.toString().length;
if (urlLength > _urlLengthLimit) {
_log.warning(
'Request URL is $urlLength characters, which exceeds the limit of $_urlLengthLimit. '
'If selecting many fields, consider using a view. '
'If filtering with large arrays, consider using an RPC function.',
);
}

try {
if (method == null) {
throw ArgumentError(
'Missing table operation: select, insert, update or delete',
);
}

if (_schema == null) {
final schema = _schema;
if (schema == null) {
// skip
} else if (method == _HttpMethod.get || method == _HttpMethod.head) {
execHeaders['Accept-Profile'] = _schema;
execHeaders['Accept-Profile'] = schema;
} else {
execHeaders['Content-Profile'] = _schema;
execHeaders['Content-Profile'] = schema;
}
if (method != _HttpMethod.get && method != _HttpMethod.head) {
execHeaders['Content-Type'] = 'application/json';
Expand Down Expand Up @@ -213,7 +238,11 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
method == _HttpMethod.get || method == _HttpMethod.head;

if (!_retryEnabled || !isRetryableMethod) {
return send();
final responseFuture = send();
if (_timeout != null) {
return responseFuture.timeout(Duration(milliseconds: _timeout));
}
return responseFuture;
}

for (var attempt = 0; attempt <= maxRetries; attempt++) {
Expand All @@ -222,7 +251,10 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
}

try {
final response = await send();
final responseFuture = send();
final response = _timeout != null
? await responseFuture.timeout(Duration(milliseconds: _timeout))
: await responseFuture;
if (!retryableStatusCodes.contains(response.statusCode) ||
attempt == maxRetries) {
return response;
Expand Down Expand Up @@ -253,8 +285,9 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
body = response.body;
} else {
try {
if ((response.contentLength ?? 0) > 10000 && _isolate != null) {
body = await _isolate.decode(response.body);
final isolate = _isolate;
if ((response.contentLength ?? 0) > 10000 && isolate != null) {
body = await isolate.decode(response.body);
} else {
body = jsonDecode(response.body);
}
Expand Down Expand Up @@ -308,16 +341,17 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
}
body as R;

if (_converter != null) {
converted = _converter(body);
final converter = _converter;
if (converter != null) {
converted = converter(body);
} else {
converted = body as S;
}

if (_count != null && method != _HttpMethod.head) {
if (count != null && method != _HttpMethod.head) {
return PostgrestResponse<S>(
data: converted,
count: count!,
count: count,
) as T;
} else {
return converted as T;
Expand Down Expand Up @@ -369,17 +403,18 @@ class PostgrestBuilder<T, S, R> implements Future<T> {
) {
if (error.details is String &&
error.details.toString().contains('Results contain 0 rows')) {
final converter = _converter;
if (_count != null &&
response.request!.method != _HttpMethod.head.value) {
if (_converter != null) {
return PostgrestResponse<S>(data: _converter(null as R), count: 0)
if (converter != null) {
return PostgrestResponse<S>(data: converter(null as R), count: 0)
as T;
} else {
return null as T;
}
} else {
if (_converter != null) {
return _converter(null as R) as T;
if (converter != null) {
return converter(null as R) as T;
} else {
return null as T;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/postgrest/lib/src/postgrest_query_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class PostgrestQueryBuilder<T> extends RawPostgrestBuilder<T, T, T> {
YAJsonIsolate? isolate,
bool retryEnabled = true,
Duration Function(int attempt)? retryDelay,
int? timeout,
int urlLengthLimit = 8000,
}) : super(
PostgrestBuilder(
url: url,
Expand All @@ -32,6 +34,8 @@ class PostgrestQueryBuilder<T> extends RawPostgrestBuilder<T, T, T> {
isolate: isolate,
retryEnabled: retryEnabled,
retryDelay: retryDelay,
timeout: timeout,
urlLengthLimit: urlLengthLimit,
),
);

Expand Down Expand Up @@ -275,6 +279,8 @@ class PostgrestQueryBuilder<T> extends RawPostgrestBuilder<T, T, T> {
isolate: _isolate,
retryEnabled: enabled,
retryDelay: _retryDelay,
timeout: _timeout,
urlLengthLimit: _urlLengthLimit,
);
}

Expand All @@ -289,6 +295,8 @@ class PostgrestQueryBuilder<T> extends RawPostgrestBuilder<T, T, T> {
isolate: _isolate,
retryEnabled: _retryEnabled,
retryDelay: _retryDelay,
timeout: _timeout,
urlLengthLimit: _urlLengthLimit,
);
}
}
4 changes: 4 additions & 0 deletions packages/postgrest/lib/src/postgrest_rpc_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ class PostgrestRpcBuilder extends RawPostgrestBuilder {
required YAJsonIsolate isolate,
bool retryEnabled = true,
Duration Function(int attempt)? retryDelay,
int? timeout,
int urlLengthLimit = 8000,
}) : super(
PostgrestBuilder(
url: Uri.parse(url),
Expand All @@ -18,6 +20,8 @@ class PostgrestRpcBuilder extends RawPostgrestBuilder {
isolate: isolate,
retryEnabled: retryEnabled,
retryDelay: retryDelay,
timeout: timeout,
urlLengthLimit: urlLengthLimit,
),
);

Expand Down
6 changes: 6 additions & 0 deletions packages/postgrest/lib/src/raw_postgrest_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ class RawPostgrestBuilder<T, S, R> extends PostgrestBuilder<T, S, R> {
converter: builder._converter,
retryEnabled: builder._retryEnabled,
retryDelay: builder._retryDelay,
timeout: builder._timeout,
urlLengthLimit: builder._urlLengthLimit,
);

/// Very similar to [_copyWith], but allows changing the generics, therefore [_converter] is omitted
Expand All @@ -42,6 +44,8 @@ class RawPostgrestBuilder<T, S, R> extends PostgrestBuilder<T, S, R> {
maybeSingle: maybeSingle ?? _maybeSingle,
retryEnabled: _retryEnabled,
retryDelay: _retryDelay,
timeout: _timeout,
urlLengthLimit: _urlLengthLimit,
));
}

Expand Down Expand Up @@ -77,6 +81,8 @@ class RawPostgrestBuilder<T, S, R> extends PostgrestBuilder<T, S, R> {
converter: converter,
retryEnabled: _retryEnabled,
retryDelay: _retryDelay,
timeout: _timeout,
urlLengthLimit: _urlLengthLimit,
);
}
}
Loading
Loading