diff --git a/docs/c-api/mg_util.h/mg_url_encode.md b/docs/c-api/mg_util.h/mg_url_encode.md
index 105ce9473eb7b005411b04081361e92461aab443..176ac5f7a76c4033ce1e84012cfe109d185849f0 100644
--- a/docs/c-api/mg_util.h/mg_url_encode.md
+++ b/docs/c-api/mg_util.h/mg_url_encode.md
@@ -6,8 +6,5 @@ signature: |
   struct mg_str mg_url_encode(const struct mg_str src);
 ---
 
-URL-escape the specified string.
-All non-printable characters are escaped, plus `._-$,;~()/`.
-Input need not be NUL-terminated, but the returned string is.
-Returned string is heap-allocated and must be free()'d. 
+Same as `mg_url_encode_opt(src, "._-$,;~()/", 0)`. 
 
diff --git a/mongoose.c b/mongoose.c
index 534009b745a67d2ed9d62d273dd325140a38bc52..1eb690e719d61967fc7e924b0eb8ca572c314763 100644
--- a/mongoose.c
+++ b/mongoose.c
@@ -10444,17 +10444,21 @@ void mg_basic_auth_header(const struct mg_str user, const struct mg_str pass,
   mbuf_append(buf, header_suffix, strlen(header_suffix));
 }
 
-struct mg_str mg_url_encode(const struct mg_str src) {
-  static const char *dont_escape = "._-$,;~()/";
-  static const char *hex = "0123456789abcdef";
+struct mg_str mg_url_encode_opt(const struct mg_str src,
+                                const struct mg_str safe, unsigned int flags) {
+  const char *hex =
+      (flags & MG_URL_ENCODE_F_UPPERCASE_HEX ? "0123456789ABCDEF"
+                                             : "0123456789abcdef");
   size_t i = 0;
   struct mbuf mb;
   mbuf_init(&mb, src.len);
 
   for (i = 0; i < src.len; i++) {
     const unsigned char c = *((const unsigned char *) src.p + i);
-    if (isalnum(c) || strchr(dont_escape, c) != NULL) {
+    if (isalnum(c) || mg_strchr(safe, c) != NULL) {
       mbuf_append(&mb, &c, 1);
+    } else if (c == ' ' && (flags & MG_URL_ENCODE_F_SPACE_AS_PLUS)) {
+      mbuf_append(&mb, "+", 1);
     } else {
       mbuf_append(&mb, "%", 1);
       mbuf_append(&mb, &hex[c >> 4], 1);
@@ -10465,6 +10469,10 @@ struct mg_str mg_url_encode(const struct mg_str src) {
   mbuf_trim(&mb);
   return mg_mk_str_n(mb.buf, mb.len - 1);
 }
+
+struct mg_str mg_url_encode(const struct mg_str src) {
+  return mg_url_encode_opt(src, mg_mk_str("._-$,;~()/"), 0);
+}
 #ifdef MG_MODULE_LINES
 #line 1 "mongoose/src/mg_mqtt.c"
 #endif
diff --git a/mongoose.h b/mongoose.h
index 080642c3b2c9ab83d54295c2b810fa6e3bde0d26..a02d96466857dac372ce5d5f92502e35046441b0 100644
--- a/mongoose.h
+++ b/mongoose.h
@@ -2314,6 +2314,14 @@ void cs_base64_finish(struct cs_base64_ctx *ctx);
 
 void cs_base64_encode(const unsigned char *src, int src_len, char *dst);
 void cs_fprint_base64(FILE *f, const unsigned char *src, int src_len);
+
+/*
+ * Decodes a base64 string `s` length `len` into `dst`.
+ * `dst` must have enough space to hold the result.
+ * `*dec_len` will contain the resulting length of the string in `dst`
+ * while return value will return number of processed bytes in `src`.
+ * Return value == len indicates successful processing of all the data.
+ */
 int cs_base64_decode(const unsigned char *s, int len, char *dst, int *dec_len);
 
 #ifdef __cplusplus
@@ -4501,10 +4509,17 @@ void mg_basic_auth_header(const struct mg_str user, const struct mg_str pass,
 
 /*
  * URL-escape the specified string.
- * All non-printable characters are escaped, plus `._-$,;~()/`.
+ * All characters acept letters, numbers and characters listed in
+ * `safe` are escaped. If `hex_upper`is true, `A-F` are used for hex digits.
  * Input need not be NUL-terminated, but the returned string is.
  * Returned string is heap-allocated and must be free()'d.
  */
+#define MG_URL_ENCODE_F_SPACE_AS_PLUS (1 << 0)
+#define MG_URL_ENCODE_F_UPPERCASE_HEX (1 << 1)
+struct mg_str mg_url_encode_opt(const struct mg_str src,
+                                const struct mg_str safe, unsigned int flags);
+
+/* Same as `mg_url_encode_opt(src, "._-$,;~()/", 0)`. */
 struct mg_str mg_url_encode(const struct mg_str src);
 
 #ifdef __cplusplus
diff --git a/src/mg_util.c b/src/mg_util.c
index 95246a8dbe7335d711b6c451412ebd9e7f55f2a0..cfbd87e5a5736c1e5592e79c233ca5c4983f6cfa 100644
--- a/src/mg_util.c
+++ b/src/mg_util.c
@@ -313,17 +313,21 @@ void mg_basic_auth_header(const struct mg_str user, const struct mg_str pass,
   mbuf_append(buf, header_suffix, strlen(header_suffix));
 }
 
-struct mg_str mg_url_encode(const struct mg_str src) {
-  static const char *dont_escape = "._-$,;~()/";
-  static const char *hex = "0123456789abcdef";
+struct mg_str mg_url_encode_opt(const struct mg_str src,
+                                const struct mg_str safe, unsigned int flags) {
+  const char *hex =
+      (flags & MG_URL_ENCODE_F_UPPERCASE_HEX ? "0123456789ABCDEF"
+                                             : "0123456789abcdef");
   size_t i = 0;
   struct mbuf mb;
   mbuf_init(&mb, src.len);
 
   for (i = 0; i < src.len; i++) {
     const unsigned char c = *((const unsigned char *) src.p + i);
-    if (isalnum(c) || strchr(dont_escape, c) != NULL) {
+    if (isalnum(c) || mg_strchr(safe, c) != NULL) {
       mbuf_append(&mb, &c, 1);
+    } else if (c == ' ' && (flags & MG_URL_ENCODE_F_SPACE_AS_PLUS)) {
+      mbuf_append(&mb, "+", 1);
     } else {
       mbuf_append(&mb, "%", 1);
       mbuf_append(&mb, &hex[c >> 4], 1);
@@ -334,3 +338,7 @@ struct mg_str mg_url_encode(const struct mg_str src) {
   mbuf_trim(&mb);
   return mg_mk_str_n(mb.buf, mb.len - 1);
 }
+
+struct mg_str mg_url_encode(const struct mg_str src) {
+  return mg_url_encode_opt(src, mg_mk_str("._-$,;~()/"), 0);
+}
diff --git a/src/mg_util.h b/src/mg_util.h
index e6f2e48130c43cbe9f15111f4277c9477a1582c8..8ddeb770fdec2f50b1d4f6f11116f41bb5826223 100644
--- a/src/mg_util.h
+++ b/src/mg_util.h
@@ -196,10 +196,17 @@ void mg_basic_auth_header(const struct mg_str user, const struct mg_str pass,
 
 /*
  * URL-escape the specified string.
- * All non-printable characters are escaped, plus `._-$,;~()/`.
+ * All characters acept letters, numbers and characters listed in
+ * `safe` are escaped. If `hex_upper`is true, `A-F` are used for hex digits.
  * Input need not be NUL-terminated, but the returned string is.
  * Returned string is heap-allocated and must be free()'d.
  */
+#define MG_URL_ENCODE_F_SPACE_AS_PLUS (1 << 0)
+#define MG_URL_ENCODE_F_UPPERCASE_HEX (1 << 1)
+struct mg_str mg_url_encode_opt(const struct mg_str src,
+                                const struct mg_str safe, unsigned int flags);
+
+/* Same as `mg_url_encode_opt(src, "._-$,;~()/", 0)`. */
 struct mg_str mg_url_encode(const struct mg_str src);
 
 #ifdef __cplusplus
diff --git a/test/unit_test.c b/test/unit_test.c
index f496b27c5517b16eec55798f0a4ace79d338f5b3..485403595d0c3df66c6d68c875aa93f70a40852b 100644
--- a/test/unit_test.c
+++ b/test/unit_test.c
@@ -928,10 +928,38 @@ static const char *test_mg_uri_to_local_path(void) {
 static const char *test_mg_url_encode(void) {
   const struct mg_str encode_me =
       MG_MK_STR("I'm a.little_tea-pot,here's$my;spout~oink(oink)oink/!");
-  struct mg_str encoded = mg_url_encode(encode_me);
-  ASSERT_MG_STREQ(
-      encoded, "I%27m%20a.little_tea-pot,here%27s$my;spout~oink(oink)oink/%21");
-  free((void *) encoded.p);
+  {
+    struct mg_str encoded = mg_url_encode(encode_me);
+    ASSERT_MG_STREQ(
+        encoded,
+        "I%27m%20a.little_tea-pot,here%27s$my;spout~oink(oink)oink/%21");
+    free((void *) encoded.p);
+  }
+  {
+    struct mg_str encoded = mg_url_encode_opt(encode_me, mg_mk_str(NULL), 0);
+    ASSERT_MG_STREQ(encoded,
+                    "I%27m%20a%2elittle%5ftea%2dpot%2chere%27s%24my%3bspout%"
+                    "7eoink%28oink%29oink%2f%21");
+    free((void *) encoded.p);
+  }
+  {
+    struct mg_str encoded = mg_url_encode_opt(encode_me, mg_mk_str(" /!"),
+                                              MG_URL_ENCODE_F_UPPERCASE_HEX);
+    ASSERT_MG_STREQ(encoded,
+                    "I%27m "
+                    "a%2Elittle%5Ftea%2Dpot%2Chere%27s%24my%3Bspout%7Eoink%"
+                    "28oink%29oink/!");
+    free((void *) encoded.p);
+  }
+  {
+    struct mg_str encoded = mg_url_encode_opt(
+        encode_me, mg_mk_str("/!"),
+        MG_URL_ENCODE_F_SPACE_AS_PLUS | MG_URL_ENCODE_F_UPPERCASE_HEX);
+    ASSERT_MG_STREQ(encoded,
+                    "I%27m+a%2Elittle%5Ftea%2Dpot%2Chere%27s%24my%3Bspout%"
+                    "7Eoink%28oink%29oink/!");
+    free((void *) encoded.p);
+  }
   return NULL;
 }