diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,9 @@
+## Modern URI 0.1.2.0
+
+* Fixed handling of `+` in query strings. Now `+` is parsed as space and
+  serialized as `%2b` as per RFC 1866 (paragraph 8.2.1). White space in
+  query parameters is serialized as `+`.
+
 ## Modern URI 0.1.1.1
 
 * Fixed implementation of `Text.URI.Lens.queryParam` traversal.
diff --git a/Text/URI/Parser/ByteString.hs b/Text/URI/Parser/ByteString.hs
--- a/Text/URI/Parser/ByteString.hs
+++ b/Text/URI/Parser/ByteString.hs
@@ -226,11 +226,12 @@
 pchar' = choice
   [ unreservedChar
   , percentEncChar
+  , char 43 >> pure 32
   , oneOf s <?> "sub-delimiter"
   , char 58
   , char 64 ]
   where
-    s = E.fromList (fromIntegral . ord <$> "!$'()*+,;")
+    s = E.fromList (fromIntegral . ord <$> "!$'()*,;")
 {-# INLINE pchar' #-}
 
 isAsciiAlpha :: Word8 -> Bool
diff --git a/Text/URI/Parser/Text/Utils.hs b/Text/URI/Parser/Text/Utils.hs
--- a/Text/URI/Parser/Text/Utils.hs
+++ b/Text/URI/Parser/Text/Utils.hs
@@ -151,11 +151,12 @@
 pchar' = choice
   [ unreservedChar
   , percentEncChar
+  , char '+' >> pure ' '
   , oneOf s <?> "sub-delimiter"
   , char ':'
   , char '@' ]
   where
-    s = E.fromList "!$'()*+,;"
+    s = E.fromList "!$'()*,;"
 {-# INLINE pchar' #-}
 
 isAsciiAlpha :: Char -> Bool
diff --git a/Text/URI/Render.hs b/Text/URI/Render.hs
--- a/Text/URI/Render.hs
+++ b/Text/URI/Render.hs
@@ -13,6 +13,7 @@
 {-# LANGUAGE DataKinds           #-}
 {-# LANGUAGE KindSignatures      #-}
 {-# LANGUAGE LambdaCase          #-}
+{-# LANGUAGE MultiWayIf          #-}
 {-# LANGUAGE OverloadedStrings   #-}
 {-# LANGUAGE RankNTypes          #-}
 {-# LANGUAGE RecordWildCards     #-}
@@ -180,27 +181,35 @@
       case B.uncons bs' of
         Nothing -> Nothing
         Just (w, bs'') -> Just $
-          if nne w
-            then (chr (fromIntegral w), (bs'', []))
-            else let c:|cs = encodeByte w
-                 in (c, (bs'', cs))
+          if | sap && w == 32 -> ('+', (bs'', []))
+             | nne w          -> (chr (fromIntegral w), (bs'', []))
+             | otherwise      ->
+               let c:|cs = encodeByte w
+               in (c, (bs'', cs))
     f (bs', x:xs) = Just (x, (bs', xs))
     encodeByte x = '%' :| [intToDigit h, intToDigit l]
       where
         (h, l) = fromIntegral x `quotRem` 16
     nne = needsNoEscaping (Proxy :: Proxy l)
+    sap = spaceAsPlus     (Proxy :: Proxy l)
     txt = unRText rtxt
+{-# INLINE percentEncode #-}
 
--- | This type class attaches a predicate that tells which bytes should not
--- be percent-encoded to the type level label of kind 'RTextLabel'.
+-- | This type class attaches some predicates that control serialization to
+-- the type level label of kind 'RTextLabel'.
 
 class RLabel (l :: RTextLabel) where
 
-  -- | The predicate selects bytes that do not to be percent-escaped in
+  -- | The predicate selects bytes that are not to be percent-escaped in
   -- rendered URI.
 
   needsNoEscaping :: Proxy l -> Word8 -> Bool
 
+  -- | Whether to serialize space as the plus sign.
+
+  spaceAsPlus :: Proxy l -> Bool
+  spaceAsPlus Proxy = False
+
   -- | Whether to skip percent-escaping altogether for this value.
 
   skipEscaping :: Proxy l -> Text -> Bool
@@ -226,10 +235,12 @@
 instance RLabel 'QueryKey where
   needsNoEscaping Proxy x =
     isPChar isDelim' x || x == 47 || x == 63
+  spaceAsPlus Proxy = True
 
 instance RLabel 'QueryValue where
   needsNoEscaping Proxy x =
     isPChar isDelim' x || x == 47 || x == 63
+  spaceAsPlus Proxy = True
 
 instance RLabel 'Fragment where
   needsNoEscaping Proxy x =
@@ -268,6 +279,7 @@
 isDelim' x
   | x == 33            = True
   | x == 36            = True
-  | x >= 39 && x <= 44 = True
+  | x >= 39 && x <= 42 = True
+  | x == 44            = True
   | x == 59            = True
   | otherwise          = False
diff --git a/Text/URI/Types.hs b/Text/URI/Types.hs
--- a/Text/URI/Types.hs
+++ b/Text/URI/Types.hs
@@ -86,7 +86,8 @@
   , uriPath :: [RText 'PathPiece]
     -- ^ Path
   , uriQuery :: [QueryParam]
-    -- ^ Query parameters
+    -- ^ Query parameters, RFC 3986 does not define the inner organization
+    -- of query string, so we deconstruct it following RFC 1866 here
   , uriFragment :: Maybe (RText 'Fragment)
     -- ^ Fragment, without @#@
   } deriving (Show, Eq, Ord, Data, Typeable, Generic)
diff --git a/modern-uri.cabal b/modern-uri.cabal
--- a/modern-uri.cabal
+++ b/modern-uri.cabal
@@ -1,7 +1,7 @@
 name:                 modern-uri
-version:              0.1.1.1
+version:              0.1.2.0
 cabal-version:        >= 1.18
-tested-with:          GHC==7.10.3, GHC==8.0.2, GHC==8.2.1
+tested-with:          GHC==7.10.3, GHC==8.0.2, GHC==8.2.2
 license:              BSD3
 license-file:         LICENSE.md
 author:               Mark Karpov <markkarpov92@gmail.com>
diff --git a/tests/Text/URISpec.hs b/tests/Text/URISpec.hs
--- a/tests/Text/URISpec.hs
+++ b/tests/Text/URISpec.hs
@@ -200,9 +200,9 @@
   username <- URI.mkUsername "mark:@"
   password <- URI.mkPassword "secret:@"
   host     <- URI.mkHost "github.com"
-  path     <- mapM URI.mkPathPiece ["mrkkrp", "modern-uri:@"]
+  path     <- mapM URI.mkPathPiece ["mrkkrp", "modern-uri+:@"]
   k        <- URI.mkQueryKey "foo:@"
-  v        <- URI.mkQueryValue "bar:@"
+  v        <- URI.mkQueryValue "bar :@"
   fragment <- URI.mkFragment "fragment:@"
   return URI
     { uriScheme = Just scheme
@@ -219,7 +219,7 @@
 -- | Polymorphic textual rendering of the 'URI' generated by 'mkTestURI'.
 
 testURI :: IsString a => a
-testURI = "https://mark%3a%40:secret:%40@github.com:443/mrkkrp/modern-uri%3a@?foo:@=bar:@#fragment:@"
+testURI = "https://mark%3a%40:secret:%40@github.com:443/mrkkrp/modern-uri+%3a@?foo:@=bar+:@#fragment:@"
 
 -- | A utility wrapper around 'URI.parser'.
 
