Skip to content

feat: use qs_dart in buildUrlString for enhanced query string encoding #158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jul 22, 2025
Merged
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
89 changes: 28 additions & 61 deletions lib/utils/query_parameters.dart
Original file line number Diff line number Diff line change
@@ -1,69 +1,36 @@
import 'package:qs_dart/qs_dart.dart' as qs;
import 'package:validators/validators.dart' as validators;

/// Takes a string and appends [parameters] as query parameters of [url].
///
/// It validates the URL structure and properly encodes both keys and values
/// to prevent URL injection attacks.
/// Throws [ArgumentError] if [url] is not a valid URL.
String buildUrlString(String url, Map<String, dynamic>? parameters) {
// Avoids unnecessary processing.
if (parameters == null) return url;

// Check if there are parameters to add.
if (parameters.isNotEmpty) {
// Validate URL structure to prevent injection
// First check if it looks like a valid HTTP/HTTPS URL
if (!url.startsWith('http://') && !url.startsWith('https://')) {
throw ArgumentError(
'Invalid URL structure: $url - must be a valid HTTP/HTTPS URL',
);
}

try {
final uri = Uri.parse(url);
// Additional validation: ensure it has a host
if (uri.host.isEmpty) {
throw ArgumentError(
'Invalid URL structure: $url - must have a valid host',
);
}
} catch (e) {
if (e is ArgumentError) {
rethrow;
}
throw ArgumentError('Invalid URL structure: $url');
}
late final Uri uri;

// Checks if the string url already has parameters.
if (url.contains("?")) {
url += "&";
} else {
url += "?";
try {
if (!validators.isURL(url)) {
throw FormatException('Invalid URL format');
}

// Concat every parameter to the string url with proper encoding
parameters.forEach((key, value) {
// Encode the key to prevent injection
final encodedKey = Uri.encodeQueryComponent(key);

if (value is List) {
if (value is List<String>) {
for (String singleValue in value) {
url += "$encodedKey=${Uri.encodeQueryComponent(singleValue)}&";
}
} else {
for (dynamic singleValue in value) {
url +=
"$encodedKey=${Uri.encodeQueryComponent(singleValue.toString())}&";
}
}
} else if (value is String) {
url += "$encodedKey=${Uri.encodeQueryComponent(value)}&";
} else {
url += "$encodedKey=${Uri.encodeQueryComponent(value.toString())}&";
}
});

// Remove last '&' character.
url = url.substring(0, url.length - 1);
uri = Uri.parse(url);
} on FormatException {
throw ArgumentError.value(url, 'url', 'Must be a valid URL');
}

return url;
return parameters?.isNotEmpty ?? false
? uri
.replace(
query: qs.encode(
<String, dynamic>{
...uri.queryParametersAll,
...?parameters,
},
qs.EncodeOptions(
listFormat: qs.ListFormat.repeat,
skipNulls: false,
strictNullHandling: false,
),
),
queryParameters: null)
.toString()
: url;
}
2 changes: 2 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ environment:

dependencies:
http: ^1.2.1
qs_dart: ^1.3.8
validators: ^3.0.0

dev_dependencies:
lints: ^4.0.0
Expand Down
Loading
Loading