diff --git a/CHANGELOG.md b/CHANGELOG.md
index 24f18e16f2eeb85bc1647c80b72770f4d6a0db24..5b18d25388b0a8ad7a5e0ef0f2162276c9474b3d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+# 3.0.0
+- Fixes signing error in case object name contains symbols [#29]
+
 # 2.1.0-pre
 - `getObject` now returns `MinioByteStream` with an additional `contentLength` field.
 
@@ -77,3 +80,5 @@
 ## 0.1.0
 
 - Initial version, created by Stagehand
+
+[#29]: https://github.com/xtyxtyx/minio-dart/issues/29
\ No newline at end of file
diff --git a/lib/src/minio_helpers.dart b/lib/src/minio_helpers.dart
index 0eeebe48a99e6f0443f4110b7e05a898e16ecfeb..bebc4ff5ff8c35bb5617512e3bc18d086d738328 100644
--- a/lib/src/minio_helpers.dart
+++ b/lib/src/minio_helpers.dart
@@ -1,3 +1,4 @@
+import 'package:convert/convert.dart';
 import 'package:http/http.dart';
 import 'package:mime/mime.dart' show lookupMimeType;
 import 'package:minio/src/minio_errors.dart';
@@ -231,3 +232,41 @@ void validate(Response response, {int? expect}) {
         '$expect expected, got ${response.statusCode}', null, response);
   }
 }
+
+final _a = 'a'.codeUnitAt(0);
+final _A = 'A'.codeUnitAt(0);
+final _z = 'z'.codeUnitAt(0);
+final _Z = 'Z'.codeUnitAt(0);
+final _0 = '0'.codeUnitAt(0);
+final _9 = '9'.codeUnitAt(0);
+
+final _pathIgnoredChars = {
+  '%'.codeUnitAt(0),
+  '-'.codeUnitAt(0),
+  '_'.codeUnitAt(0),
+  '.'.codeUnitAt(0),
+  '~'.codeUnitAt(0),
+  '/'.codeUnitAt(0),
+};
+
+/// encode [uri].path to HTML hex escape sequence
+String encodePath(Uri uri) {
+  final result = StringBuffer();
+  for (var char in uri.path.codeUnits) {
+    if (_A <= char && char <= _Z ||
+        _a <= char && char <= _z ||
+        _0 <= char && char <= _9) {
+      result.writeCharCode(char);
+      continue;
+    }
+
+    if (_pathIgnoredChars.contains(char)) {
+      result.writeCharCode(char);
+      continue;
+    }
+
+    result.write('%');
+    result.write(hex.encode([char]).toUpperCase());
+  }
+  return result.toString();
+}
diff --git a/lib/src/minio_sign.dart b/lib/src/minio_sign.dart
index a1fc9fefe71101e7ede5ddc68d1de7d49d00a864..9f62262d0f38e17256c3d5a678dbe62aa233f93f 100644
--- a/lib/src/minio_sign.dart
+++ b/lib/src/minio_sign.dart
@@ -44,7 +44,7 @@ String getCanonicalRequest(
   List<String> signedHeaders,
   String hashedPayload,
 ) {
-  final requestResource = request.url.path;
+  final requestResource = encodePath(request.url);
   final headers = signedHeaders.map(
     (header) => '${header.toLowerCase()}:${request.headers[header]}',
   );
diff --git a/test/minio_test.dart b/test/minio_test.dart
index 02ab20b8d22cfac095577cd1dea6c52d0deed9c0..3f00c45622b7159ea6e560ba4d5661026847f915 100644
--- a/test/minio_test.dart
+++ b/test/minio_test.dart
@@ -362,7 +362,6 @@ void testPutObject() {
   group('putObject()', () {
     final minio = getMinioClient();
     final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
-    final objectName = DateTime.now().microsecondsSinceEpoch.toString();
     final objectData = Uint8List.fromList([1, 2, 3]);
 
     setUpAll(() async {
@@ -374,6 +373,16 @@ void testPutObject() {
     });
 
     test('succeeds', () async {
+      final objectName = DateTime.now().microsecondsSinceEpoch.toString();
+      await minio.putObject(bucketName, objectName, Stream.value(objectData));
+      final stat = await minio.statObject(bucketName, objectName);
+      expect(stat.size, equals(objectData.length));
+      await minio.removeObject(bucketName, objectName);
+    });
+
+    test('works with object names with symbols', () async {
+      final objectName =
+          DateTime.now().microsecondsSinceEpoch.toString() + r'-._~,!@#$%^&*()';
       await minio.putObject(bucketName, objectName, Stream.value(objectData));
       final stat = await minio.statObject(bucketName, objectName);
       expect(stat.size, equals(objectData.length));