From 9c820b814880861754afd7457b189b132e6e24a5 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Thu, 13 Aug 2026 15:24:18 +0200 Subject: [PATCH 01/27] ci.yml: use custom project file for building with GHC 7.10.3 --- .github/workflows/ci.yml | 6 +----- cabal.7.10.3.project | 9 +++++++++ 2 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 cabal.7.10.3.project diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2bcda4f..591bffb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,11 +39,7 @@ jobs: - name: Build run: | - cabal build --ghc-option='-Wall' \ - --constraint="bytestring==0.10.6.0" \ - --constraint="array==0.5.1.0" \ - --constraint="case-insensitive==1.2.0.2" \ - --constraint="text==1.2.0.2" + cabal build --ghc-option='-Wall' --project-file=cabal.7.10.3.project cabal: name: cabal / ghc-${{matrix.ghc}} / ${{ matrix.os }} diff --git a/cabal.7.10.3.project b/cabal.7.10.3.project new file mode 100644 index 0000000..95069dc --- /dev/null +++ b/cabal.7.10.3.project @@ -0,0 +1,9 @@ +packages: ./http-types.cabal +ignore-project: False +allow-newer: Cabal-3.14.2.0:containers, + Cabal-syntax-3.14.2.0:containers +jobs: 2 +constraints: bytestring == 0.10.6.0, + case-insensitive == 1.2.0.2, + text == 1.2.0.2 +tests: True From 96b36c2c20c31edbffde9c27cfbd6971347c3402 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 14:13:05 +0200 Subject: [PATCH 02/27] use 'word8' in 'Builder's, more hex usage, and some simplification/documentation --- Network/HTTP/Types/Header.hs | 52 ++++++++------- Network/HTTP/Types/QueryLike.hs | 10 +-- Network/HTTP/Types/Status.hs | 7 ++- Network/HTTP/Types/URI.hs | 108 +++++++++++++++++--------------- 4 files changed, 96 insertions(+), 81 deletions(-) diff --git a/Network/HTTP/Types/Header.hs b/Network/HTTP/Types/Header.hs index a7e95fa..e4f75dc 100644 --- a/Network/HTTP/Types/Header.hs +++ b/Network/HTTP/Types/Header.hs @@ -90,13 +90,21 @@ module Network.HTTP.Types.Header ( ) where +import Control.Monad (guard) import qualified Data.ByteString as B -import qualified Data.ByteString.Builder as B +import qualified Data.ByteString.Builder as B ( + Builder, + byteString, + integerDec, + toLazyByteString, + word8, + ) import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy as BL import qualified Data.CaseInsensitive as CI import Data.Data (Data) import Data.List (intersperse) +import Data.Word (Word8) import GHC.Generics (Generic) -- | A full HTTP header field with the name and value separated. @@ -483,9 +491,9 @@ data ByteRange -- -- @since 0.6.11 renderByteRangeBuilder :: ByteRange -> B.Builder -renderByteRangeBuilder (ByteRangeFrom from) = B.integerDec from `mappend` B.char7 '-' -renderByteRangeBuilder (ByteRangeFromTo from to) = B.integerDec from `mappend` B.char7 '-' `mappend` B.integerDec to -renderByteRangeBuilder (ByteRangeSuffix suffix) = B.char7 '-' `mappend` B.integerDec suffix +renderByteRangeBuilder (ByteRangeFrom from) = B.integerDec from `mappend` B.word8 _hyphen +renderByteRangeBuilder (ByteRangeFromTo from to) = B.integerDec from `mappend` B.word8 _hyphen `mappend` B.integerDec to +renderByteRangeBuilder (ByteRangeSuffix suffix) = B.word8 _hyphen `mappend` B.integerDec suffix -- | Renders a byte range into a 'B.ByteString'. -- @@ -507,7 +515,7 @@ type ByteRanges = [ByteRange] renderByteRangesBuilder :: ByteRanges -> B.Builder renderByteRangesBuilder xs = B.byteString "bytes=" - `mappend` mconcat (intersperse (B.char7 ',') $ map renderByteRangeBuilder xs) + `mappend` mconcat (intersperse (B.word8 _comma) $ map renderByteRangeBuilder xs) -- | Renders a list of byte ranges into a 'B.ByteString'. -- @@ -540,33 +548,35 @@ renderByteRanges = BL.toStrict . B.toLazyByteString . renderByteRangesBuilder -- @since 0.9.1 parseByteRanges :: B.ByteString -> Maybe ByteRanges parseByteRanges bs1 = do - bs2 <- stripPrefixB "bytes=" bs1 - (r, bs3) <- range bs2 - ranges (r :) bs3 + (r, bs2) <- stripBytes >>= range + ranges (r :) bs2 where + stripBytes = do + let prefix = "bytes=" + prefixLen = B.length prefix + (pre, post) = B.splitAt prefixLen bs1 + guard $ pre == prefix + Just post range bs2 = do (i, bs3) <- B8.readInteger bs2 if i < 0 -- has prefix "-" ("-0" is not valid, but here treated as "0-") then Just (ByteRangeSuffix (negate i), bs3) else do - bs4 <- stripPrefixB "-" bs3 + bs4 <- pop _hyphen bs3 case B8.readInteger bs4 of Just (j, bs5) | j >= i -> Just (ByteRangeFromTo i j, bs5) _ -> Just (ByteRangeFrom i, bs4) ranges front bs3 | B.null bs3 = Just (front []) | otherwise = do - bs4 <- stripPrefixB "," bs3 + bs4 <- pop _comma bs3 (r, bs5) <- range bs4 ranges (front . (r :)) bs5 - -stripPrefixB :: B.ByteString -> B.ByteString -> Maybe B.ByteString -#if !MIN_VERSION_bytestring(0,10,8) --- FIXME: Use 'stripPrefix' from the 'bytestring' package. --- Might have to update the dependency constraints though. -stripPrefixB x y - | x `B.isPrefixOf` y = Just (B.drop (B.length x) y) - | otherwise = Nothing -#else -stripPrefixB = B.stripPrefix -#endif + pop w8 bs = do + (b, rest) <- B.uncons bs + guard $ b == w8 + Just rest + +_comma, _hyphen :: Word8 +_comma = 0x2C +_hyphen = 0x2D diff --git a/Network/HTTP/Types/QueryLike.hs b/Network/HTTP/Types/QueryLike.hs index 1c24843..fc46ca5 100644 --- a/Network/HTTP/Types/QueryLike.hs +++ b/Network/HTTP/Types/QueryLike.hs @@ -9,8 +9,8 @@ module Network.HTTP.Types.QueryLike ( where import Control.Arrow ((***)) -import Data.ByteString as B (ByteString, concat) -import Data.ByteString.Lazy as L (ByteString, toChunks) +import Data.ByteString as B (ByteString) +import Data.ByteString.Lazy as L (ByteString, toStrict) import Data.Maybe (catMaybes) import Data.Text as T (Text, pack) import Data.Text.Encoding as T (encodeUtf8) @@ -48,13 +48,13 @@ instance (QueryKeyLike k, QueryValueLike v) => QueryLike [Maybe (k, v)] where toQuery = toQuery . catMaybes instance QueryKeyLike B.ByteString where toQueryKey = id -instance QueryKeyLike L.ByteString where toQueryKey = B.concat . L.toChunks +instance QueryKeyLike L.ByteString where toQueryKey = L.toStrict instance QueryKeyLike T.Text where toQueryKey = T.encodeUtf8 instance QueryKeyLike [Char] where toQueryKey = T.encodeUtf8 . T.pack instance QueryValueLike B.ByteString where toQueryValue = Just -instance QueryValueLike L.ByteString where toQueryValue = Just . B.concat . L.toChunks +instance QueryValueLike L.ByteString where toQueryValue = Just . L.toStrict instance QueryValueLike T.Text where toQueryValue = Just . T.encodeUtf8 instance QueryValueLike [Char] where toQueryValue = Just . T.encodeUtf8 . T.pack -instance QueryValueLike a => QueryValueLike (Maybe a) where +instance (QueryValueLike a) => QueryValueLike (Maybe a) where toQueryValue mVal = mVal >>= toQueryValue diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index a939a15..d56780a 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -128,6 +128,7 @@ module Network.HTTP.Types.Status ( import Data.ByteString as B (ByteString, empty) import Data.Data (Data) +import Data.Function (on) import GHC.Generics (Generic) -- | HTTP Status. @@ -166,13 +167,13 @@ data Status = Status -- | A 'Status' is equal to another 'Status' if the status codes are equal. instance Eq Status where - Status{statusCode = a} == Status{statusCode = b} = a == b + (==) = (==) `on` statusCode -- | 'Status'es are ordered according to their status codes only. instance Ord Status where - compare Status{statusCode = a} Status{statusCode = b} = a `compare` b + compare = compare `on` statusCode --- | Be advised, that when using the \"enumFrom*\" family of methods or +-- | Be advised, that when using the @enumFrom*@ family of methods or -- ranges in lists, it will generate all possible status codes. -- -- E.g. @[status100 .. status200]@ generates 'Status'es of @100, 101, 102 .. 198, 199, 200@ diff --git a/Network/HTTP/Types/URI.hs b/Network/HTTP/Types/URI.hs index 8d34f18..d44ffc7 100644 --- a/Network/HTTP/Types/URI.hs +++ b/Network/HTTP/Types/URI.hs @@ -84,7 +84,7 @@ module Network.HTTP.Types.URI ( where import Control.Arrow (second, (***)) -import Data.Bits (shiftL, (.|.)) +import Data.Bits (shiftL, shiftR, (.&.), (.|.)) import qualified Data.ByteString as B import qualified Data.ByteString.Builder as B import qualified Data.ByteString.Lazy as BL @@ -114,6 +114,9 @@ import Data.Word (Word8) type QueryItem = (B.ByteString, Maybe B.ByteString) -- | A sequence of 'QueryItem's. +-- +-- General form: @a=b&c=d@, but if for example the value of @a@ is 'Nothing' +-- instead of @'Just' "b"@, it becomes @a&c=d@. type Query = [QueryItem] -- | Like Query, but with 'Text' instead of 'B.ByteString' (UTF8-encoded). @@ -174,21 +177,17 @@ simpleQueryToQuery = map (second Just) renderQueryBuilder :: Bool -> Query -> B.Builder renderQueryBuilder _ [] = mempty renderQueryBuilder qmark' (p : ps) = - -- FIXME: replace mconcat + map with foldr - mconcat $ - go (if qmark' then qmark else mempty) p - : map (go amp) ps + mconcat $ go qmark p : map (go amp) ps where - qmark = B.byteString "?" - amp = B.byteString "&" - equal = B.byteString "=" + qmark = if qmark' then B.word8 _question else mempty + amp = B.word8 _ampersand go sep (k, mv) = mconcat [ sep , urlEncodeBuilder True k , case mv of Nothing -> mempty - Just v -> equal `mappend` urlEncodeBuilder True v + Just v -> B.word8 _equal `mappend` urlEncodeBuilder True v ] -- | Renders the given 'Query' into a 'B.ByteString'. @@ -234,30 +233,29 @@ parseQueryReplacePlus replacePlus bs = parseQueryString' $ dropQuestion bs where dropQuestion q = case B.uncons q of - Just (63, q') -> q' + -- 0x3F == _question + Just (0x3F, q') -> q' _ -> q parseQueryString' q | B.null q = [] parseQueryString' q = let (x, xs) = breakDiscard queryStringSeparators q in parsePair x : parseQueryString' xs where + queryStringSeparators :: B.ByteString + queryStringSeparators = "&;" parsePair x = - let (k, v) = B.break (== 61) x -- equal sign + let (k, v) = B.break (== _equal) x v'' = case B.uncons v of Just (_, v') -> Just $ urlDecode replacePlus v' _ -> Nothing in (urlDecode replacePlus k, v'') - -queryStringSeparators :: B.ByteString -queryStringSeparators = B.pack [38, 59] -- ampersand, semicolon - --- | Break the second bytestring at the first occurrence of any bytes from --- the first bytestring, discarding that byte. -breakDiscard :: B.ByteString -> B.ByteString -> (B.ByteString, B.ByteString) -breakDiscard seps s = - let (x, y) = B.break (`B.elem` seps) s - in (x, B.drop 1 y) + -- Break the second bytestring at the first occurrence of any bytes from + -- the first bytestring, discarding that byte. + breakDiscard :: B.ByteString -> B.ByteString -> (B.ByteString, B.ByteString) + breakDiscard seps s = + let (x, y) = B.break (`B.elem` seps) s + in (x, B.drop 1 y) -- | Parse 'SimpleQuery' from a 'B.ByteString'. -- @@ -300,19 +298,21 @@ urlEncodeBuilder' extraUnreserved = | unreserved ch = B.word8 ch | otherwise = h2 ch + -- The order is optimized from most expected to least expected unreserved ch - | ch >= 65 && ch <= 90 = True -- A-Z - | ch >= 97 && ch <= 122 = True -- a-z - | ch >= 48 && ch <= 57 = True -- 0-9 - unreserved c = c `elem` extraUnreserved + | ch >= 0x61 && ch <= 0x7A = True -- a-z + | ch >= 0x30 && ch <= 0x39 = True -- 0-9 + | ch >= 0x41 && ch <= 0x5A = True -- A-Z + | otherwise = ch `elem` extraUnreserved -- must be upper-case - h2 v = B.word8 37 `mappend` B.word8 (h a) `mappend` B.word8 (h b) -- 37 = % + h2 v = B.word8 _percent `mappend` B.word8 (h a) `mappend` B.word8 (h b) where - (a, b) = v `divMod` 16 + a = v `shiftR` 4 + b = v .&. 0x0F h i - | i < 10 = 48 + i -- zero (0) - | otherwise = 65 + i - 10 -- 65: A + | i < 10 = 0x30 + i -- zero (0) + | otherwise = 0x41 + i - 10 -- 0x41: A -- | Percent-encoding for URLs. -- @@ -359,19 +359,19 @@ urlDecode replacePlus z = fst $ B.unfoldrN (B.length z) go z case B.uncons bs of Nothing -> Nothing -- plus to space - Just (43, ws) | replacePlus -> Just (32, ws) + Just (0x2B, ws) | replacePlus -> Just (0x20, ws) -- percent - Just (37, ws) -> Just $ fromMaybe (37, ws) $ do + Just tup@(0x25, ws) -> Just $ fromMaybe tup $ do (x, xs) <- B.uncons ws - x' <- hexVal x (y, ys) <- B.uncons xs - y' <- hexVal y - Just (combine x' y', ys) - Just (w, ws) -> Just (w, ws) + a <- hexVal x + b <- hexVal y + Just (a `combine` b, ys) + Just other -> Just other hexVal w - | 48 <= w && w <= 57 = Just $ w - 48 -- 0 - 9 - | 65 <= w && w <= 70 = Just $ w - 55 -- A - F - | 97 <= w && w <= 102 = Just $ w - 87 -- a - f + | 0x30 <= w && w <= 0x39 = Just $ w .&. 0x0F -- 0 - 9 + | 0x41 <= w && w <= 0x46 = Just $ w - 0x37 -- A - F ((w - 0x41) + 10) + | 0x61 <= w && w <= 0x66 = Just $ w - 0x57 -- a - f ((w - 0x61) + 10) | otherwise = Nothing combine :: Word8 -> Word8 -> Word8 combine a b = shiftL a 4 .|. b @@ -409,13 +409,13 @@ urlDecode replacePlus z = fst $ B.unfoldrN (B.length z) go z -- -- @since 0.5 encodePathSegments :: [Text] -> B.Builder -encodePathSegments = foldr (\x -> mappend (B.byteString "/" `mappend` encodePathSegment x)) mempty +encodePathSegments = foldr (\x -> mappend (B.word8 _slash `mappend` encodePathSegment x)) mempty -- | Like 'encodePathSegments', but without the initial slash. -- -- @since 0.6.10 encodePathSegmentsRelative :: [Text] -> B.Builder -encodePathSegmentsRelative xs = mconcat $ intersperse (B.byteString "/") (map encodePathSegment xs) +encodePathSegmentsRelative xs = mconcat $ intersperse (B.word8 _slash) (map encodePathSegment xs) encodePathSegment :: Text -> B.Builder encodePathSegment = urlEncodeBuilder False . encodeUtf8 @@ -433,10 +433,11 @@ decodePathSegments a = where drop1Slash bs = case B.uncons bs of - Just (47, bs') -> bs' -- 47 == / + -- 0x2F == _slash + Just (0x2F, bs') -> bs' _ -> bs go bs = - let (x, y) = B.break (== 47) bs + let (x, y) = B.break (== _slash) bs in decodePathSegment x : if B.null y then [] @@ -478,7 +479,7 @@ extractPath = ensureNonEmpty . extract | "http://" `B.isPrefixOf` path = (snd . breakOnSlash . B.drop 7) path | "https://" `B.isPrefixOf` path = (snd . breakOnSlash . B.drop 8) path | otherwise = path - breakOnSlash = B.break (== 47) + breakOnSlash = B.break (== _slash) ensureNonEmpty "" = "/" ensureNonEmpty p = p @@ -494,7 +495,7 @@ encodePath x y = encodePathSegments x `mappend` renderQueryBuilder True y -- @since 0.5 decodePath :: B.ByteString -> ([Text], Query) decodePath b = - let (x, y) = B.break (== 63) b -- question mark + let (x, y) = B.break (== _question) b in (decodePathSegments x, parseQuery y) ----------------------------------------------------------------------------------------- @@ -545,22 +546,25 @@ renderQueryPartialEscape qm = -- @since 0.12.1 renderQueryBuilderPartialEscape :: Bool -> PartialEscapeQuery -> B.Builder renderQueryBuilderPartialEscape _ [] = mempty --- FIXME: replace mconcat + map with foldr renderQueryBuilderPartialEscape qmark' (p : ps) = - mconcat $ - go (if qmark' then qmark else mempty) p - : map (go amp) ps + mconcat $ go qmark p : map (go amp) ps where - qmark = B.byteString "?" - amp = B.byteString "&" - equal = B.byteString "=" + qmark = if qmark' then B.word8 _question else mempty + amp = B.word8 _ampersand go sep (k, mv) = mconcat [ sep , urlEncodeBuilder True k , case mv of [] -> mempty - vs -> equal `mappend` mconcat (map encode vs) + vs -> B.word8 _equal `mappend` mconcat (map encode vs) ] encode (QE v) = urlEncodeBuilder True v encode (QN v) = B.byteString v + +_percent, _ampersand, _slash, _equal, _question :: Word8 +_percent = 0x25 +_ampersand = 0x26 +_slash = 0x2F +_equal = 0x3D +_question = 0x3F From ff5ab38795f4ace516e07f8ac801adda21435052 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 15:15:23 +0200 Subject: [PATCH 03/27] test: added test for lowercase percent encoded decoding --- test/Network/HTTP/Types/URISpec.hs | 38 +++++++++++++++++++----------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/test/Network/HTTP/Types/URISpec.hs b/test/Network/HTTP/Types/URISpec.hs index 5e5591c..cb901c4 100644 --- a/test/Network/HTTP/Types/URISpec.hs +++ b/test/Network/HTTP/Types/URISpec.hs @@ -7,10 +7,12 @@ module Network.HTTP.Types.URISpec (main, spec) where +import Data.Bits ((.|.)) import qualified Data.ByteString as B import qualified Data.ByteString.Builder as BB import qualified Data.ByteString.Char8 as B8 import qualified Data.ByteString.Lazy as BL +import qualified Data.Char as C import Data.Maybe (fromMaybe) import Data.Text as T (Text, null) import Debug.Trace (traceShow) @@ -25,6 +27,7 @@ import Test.QuickCheck ( property, suchThat, (.&&.), + (===), (==>), ) import Test.QuickCheck.Instances () @@ -42,7 +45,7 @@ spec = do it "does not escape period and dash" $ toStrictBS (encodePath ["foo-bar.baz"] []) `shouldBe` "/foo-bar.baz" - -- FIXME: needs more path tests + -- FIXME: needs more path tests describe "encode/decode query" $ do it "is identity to encode and then decode" $ @@ -85,6 +88,13 @@ spec = do it "still encodes the same (query)" $ mkGoldenFile "urlEncode-query" $ urlEncode True asciis + it "decodes lower case" $ + property $ \bs -> + -- force all bytes to be above the ASCII range + let onlyPercent = B.map (.|. 0x80) bs + -- Should only be percent encoded and then all "A-F -> a-f" + lowerCaseEncoded = B8.map C.toLower $ urlEncode True onlyPercent + in urlDecode True lowerCaseEncoded === onlyPercent describe "decodePathSegments" $ do it "is inverse to encodePathSegments" $ @@ -150,15 +160,15 @@ goldenDir = "test" ".golden" mkGoldenFile :: String -> B.ByteString -> Golden B.ByteString mkGoldenFile name content = - Golden { - output = content, - encodePretty = B8.unpack, - writeToFile = B.writeFile, - readFromFile = B.readFile, - goldenFile = goldenDir name "golden", - actualFile = Just (goldenDir name "actual"), - failFirstTime = False - } + Golden + { output = content + , encodePretty = B8.unpack + , writeToFile = B.writeFile + , readFromFile = B.readFile + , goldenFile = goldenDir name "golden" + , actualFile = Just (goldenDir name "actual") + , failFirstTime = False + } propEncodeDecodePath :: ([Text], QueryGen B.ByteString) -> Bool propEncodeDecodePath (p', QueryGen b) = @@ -209,15 +219,15 @@ propDecodeSimpleQuery (QueryGen q) = where rq = renderQuery True q -propEncodeDecodeQuerySimple :: QueryGen B.ByteString -> Bool -> Bool +propEncodeDecodeQuerySimple :: QueryGen B.ByteString -> Bool -> Property propEncodeDecodeQuerySimple (QueryGen q') b = - q == (parseSimpleQuery . renderSimpleQuery b) q + q === (parseSimpleQuery . renderSimpleQuery b) q where q = fmap (fmap $ fromMaybe "") q' -propEncodeDecodeURL :: B.ByteString -> Bool -> Bool -> Bool +propEncodeDecodeURL :: B.ByteString -> Bool -> Bool -> Property propEncodeDecodeURL bs b1 b2 = - bs == urlDecode b1 (urlEncode b3 bs) + bs === urlDecode b1 (urlEncode b3 bs) where b3 = b1 || b2 From 916d6aada55c2f4c81f1a8ce1a1806b3ac16ed82 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 15:16:20 +0200 Subject: [PATCH 04/27] documentation improvements --- Network/HTTP/Types.hs | 5 +---- Network/HTTP/Types/Status.hs | 28 +++++++--------------------- Network/HTTP/Types/Version.hs | 13 +++++++++++++ 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/Network/HTTP/Types.hs b/Network/HTTP/Types.hs index 1c48629..a60c9eb 100644 --- a/Network/HTTP/Types.hs +++ b/Network/HTTP/Types.hs @@ -2,7 +2,6 @@ module Network.HTTP.Types ( -- * Methods -- | __For more information__: "Network.HTTP.Types.Method" - Method, -- ** Constants @@ -26,7 +25,6 @@ module Network.HTTP.Types ( -- * Versions -- | __For more information__: "Network.HTTP.Types.Version" - HttpVersion (..), http09, http10, @@ -37,7 +35,6 @@ module Network.HTTP.Types ( -- * Status -- | __For more information__: "Network.HTTP.Types.Status" - Status (..), -- ** Constants @@ -156,7 +153,7 @@ module Network.HTTP.Types ( RequestHeaders, ResponseHeaders, - -- ** Header constants + -- ** Constants hAccept, hAcceptCharset, hAcceptEncoding, diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index d56780a..24bbb19 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -4,16 +4,10 @@ -- | Types and constants to describe HTTP status codes. -- --- At the bottom are some functions to check if a given 'Status' is from a certain category. (i.e. @1XX@, @2XX@, etc.) +-- At the bottom are some functions to check if a given t'Status' is from a certain category. (i.e. @1XX@, @2XX@, etc.) module Network.HTTP.Types.Status ( -- * HTTP Status - - -- If we ever want to deprecate the 'Status' data constructor: - -- #if __GLASGOW_HASKELL__ >= 908 - -- {-# DEPRECATED "Use 'mkStatus' when constructing a 'Status'" #-} Status(Status) - -- #else Status (Status), - -- #endif statusCode, statusMessage, mkStatus, @@ -141,11 +135,11 @@ import GHC.Generics (Generic) -- Note that the 'Show' instance is only for debugging. data Status = Status { statusCode :: Int - -- ^ The 3-digit code of a 'Status' + -- ^ The 3-digit code of a t'Status' -- -- For example: "200" in a @200 OK@ status , statusMessage :: B.ByteString - -- ^ The textual message of a 'Status' + -- ^ The textual message of a t'Status' -- -- For example: "Not Found" in a @404 Not Found@ status } @@ -157,26 +151,18 @@ data Status = Status Generic ) --- FIXME: If the data constructor of 'Status' is ever deprecated, we should define --- a pattern synonym to minimize any breakage. This also involves changing the --- name of the constructor, so that it doesn't clash with the new pattern synonym --- that's replacing it. --- --- > data Status = MkStatus ... --- > pattern Status code msg = MkStatus code msg - --- | A 'Status' is equal to another 'Status' if the status codes are equal. +-- | A t'Status' is equal to another t'Status' if the status codes are equal. instance Eq Status where (==) = (==) `on` statusCode --- | 'Status'es are ordered according to their status codes only. +-- | t'Status'es are ordered according to their status codes only. instance Ord Status where compare = compare `on` statusCode -- | Be advised, that when using the @enumFrom*@ family of methods or -- ranges in lists, it will generate all possible status codes. -- --- E.g. @[status100 .. status200]@ generates 'Status'es of @100, 101, 102 .. 198, 199, 200@ +-- E.g. @[status100 .. status200]@ generates t'Status'es of @100, 101, 102 .. 198, 199, 200@ -- -- The statuses not included in this library will have an empty message. -- @@ -239,7 +225,7 @@ instance Bounded Status where minBound = status100 maxBound = status511 --- | Create a 'Status' from a status code and message. +-- | Create a t'Status' from a status code and message. -- -- @since 0.7.3 mkStatus :: Int -> B.ByteString -> Status diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index 6a68731..934df34 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -2,6 +2,14 @@ {-# LANGUAGE DeriveGeneric #-} -- | Types and constants to describe the HTTP version. +-- +-- There are no parsing functions, as the formats are fairly different +-- per version of HTTP. And seeing as there are only a handful of versions, +-- it is easier to manually parse the version ad hoc. +-- +-- For example, when you are expecting HTTP v1, try to match @HTTP/1.1@ or +-- @HTTP/1.0@. If you're expecting HTTP v2 or v3, you'd be using ALPN tokens, +-- which would be @http/1.1@, @h2@ or @h3@. module Network.HTTP.Types.Version ( HttpVersion (..), http09, @@ -32,6 +40,11 @@ data HttpVersion = HttpVersion -- | >>> show http11 -- "HTTP/1.1" +-- >>> show http20 +-- "HTTP/2.0" +-- +-- This should not be used to render the HTTP version, as different versions +-- have different ways of rendering (i.e. HTTP v2 uses @"h2"@ instead of @"HTTP/2.0"@) instance Show HttpVersion where show (HttpVersion major minor) = "HTTP/" ++ show major ++ "." ++ show minor From 4790ffe3665c70a00a31135b5631899a26c4f20c Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 15:17:17 +0200 Subject: [PATCH 05/27] added pattern synonyms for 'HttpVersion' constants --- Network/HTTP/Types/Version.hs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index 934df34..81131cc 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -1,5 +1,6 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE PatternSynonyms #-} -- | Types and constants to describe the HTTP version. -- @@ -11,7 +12,14 @@ -- @HTTP/1.0@. If you're expecting HTTP v2 or v3, you'd be using ALPN tokens, -- which would be @http/1.1@, @h2@ or @h3@. module Network.HTTP.Types.Version ( - HttpVersion (..), + HttpVersion ( + .., + Http09, + Http10, + Http11, + Http20, + Http30 + ), http09, http10, http11, @@ -71,3 +79,24 @@ http20 = HttpVersion 2 0 -- @since 0.12.5 http30 :: HttpVersion http30 = HttpVersion 3 0 + +---------------------- +-- Pattern Synonyms -- +---------------------- + +pattern Http09, Http10, Http11, Http20, Http30 :: HttpVersion + +-- | @since 0.12.6 +pattern Http09 = HttpVersion 0 9 + +-- | @since 0.12.6 +pattern Http10 = HttpVersion 1 0 + +-- | @since 0.12.6 +pattern Http11 = HttpVersion 1 1 + +-- | @since 0.12.6 +pattern Http20 = HttpVersion 2 0 + +-- | @since 0.12.6 +pattern Http30 = HttpVersion 3 0 From 1c598deeca432e3ea3053cc632543ea5effe4e41 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 15:17:51 +0200 Subject: [PATCH 06/27] add more header constants and unit tests for the new constants --- Network/HTTP/Types.hs | 15 ++++ Network/HTTP/Types/Header.hs | 119 ++++++++++++++++++++++++-- test/Network/HTTP/Types/HeaderSpec.hs | 15 ++++ 3 files changed, 142 insertions(+), 7 deletions(-) diff --git a/Network/HTTP/Types.hs b/Network/HTTP/Types.hs index a60c9eb..68f2258 100644 --- a/Network/HTTP/Types.hs +++ b/Network/HTTP/Types.hs @@ -158,13 +158,23 @@ module Network.HTTP.Types ( hAcceptCharset, hAcceptEncoding, hAcceptLanguage, + hAcceptPatch, hAcceptQuery, hAcceptRanges, + hAccessControlAllowCredentials, + hAccessControlAllowHeaders, + hAccessControlAllowMethods, + hAccessControlAllowOrigin, + hAccessControlExposeHeaders, + hAccessControlMaxAge, + hAccessControlRequestMethod, hAge, hAllow, + hAltSvc, hAuthorization, hCacheControl, hConnection, + hContentDigest, hContentDisposition, hContentEncoding, hContentLanguage, @@ -172,12 +182,15 @@ module Network.HTTP.Types ( hContentLocation, hContentMD5, hContentRange, + hContentSecurityPolicy, + hContentSecurityPolicyReportOnly, hContentType, hCookie, hDate, hETag, hExpect, hExpires, + hForwarded, hFrom, hHost, hIfMatch, @@ -186,6 +199,7 @@ module Network.HTTP.Types ( hIfRange, hIfUnmodifiedSince, hLastModified, + hLink, hLocation, hMaxForwards, hMIMEVersion, @@ -200,6 +214,7 @@ module Network.HTTP.Types ( hRetryAfter, hServer, hSetCookie, + hStrictTransportSecurity, hTE, hTrailer, hTransferEncoding, diff --git a/Network/HTTP/Types/Header.hs b/Network/HTTP/Types/Header.hs index e4f75dc..fcfb755 100644 --- a/Network/HTTP/Types/Header.hs +++ b/Network/HTTP/Types/Header.hs @@ -23,13 +23,23 @@ module Network.HTTP.Types.Header ( hAcceptCharset, hAcceptEncoding, hAcceptLanguage, + hAcceptPatch, hAcceptQuery, hAcceptRanges, + hAccessControlAllowCredentials, + hAccessControlAllowHeaders, + hAccessControlAllowMethods, + hAccessControlAllowOrigin, + hAccessControlExposeHeaders, + hAccessControlMaxAge, + hAccessControlRequestMethod, hAge, hAllow, + hAltSvc, hAuthorization, hCacheControl, hConnection, + hContentDigest, hContentDisposition, hContentEncoding, hContentLanguage, @@ -37,12 +47,15 @@ module Network.HTTP.Types.Header ( hContentLocation, hContentMD5, hContentRange, + hContentSecurityPolicy, + hContentSecurityPolicyReportOnly, hContentType, hCookie, hDate, hETag, hExpect, hExpires, + hForwarded, hFrom, hHost, hIfMatch, @@ -51,6 +64,7 @@ module Network.HTTP.Types.Header ( hIfRange, hIfUnmodifiedSince, hLastModified, + hLink, hLocation, hMaxForwards, hMIMEVersion, @@ -65,6 +79,7 @@ module Network.HTTP.Types.Header ( hRetryAfter, hServer, hSetCookie, + hStrictTransportSecurity, hTE, hTrailer, hTransferEncoding, @@ -145,6 +160,48 @@ hAcceptCharset = "Accept-Charset" hAcceptEncoding :: HeaderName hAcceptEncoding = "Accept-Encoding" +-- | [Access-Control-Allow-Credentials](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-origin-response-header) +-- +-- @since 0.12.6 +hAccessControlAllowCredentials :: HeaderName +hAccessControlAllowCredentials = "Access-Control-Allow-Credentials" + +-- | [Access-Control-Allow-Headers](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-headers-response-header) +-- +-- @since 0.12.6 +hAccessControlAllowHeaders :: HeaderName +hAccessControlAllowHeaders = "Access-Control-Allow-Headers" + +-- | [Access-Control-Allow-Methods](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-methods-response-header) +-- +-- @since 0.12.6 +hAccessControlAllowMethods :: HeaderName +hAccessControlAllowMethods = "Access-Control-Allow-Methods" + +-- | [Access-Control-Allow-Origin](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-origin-response-header) +-- +-- @since 0.12.6 +hAccessControlAllowOrigin :: HeaderName +hAccessControlAllowOrigin = "Access-Control-Allow-Origin" + +-- | [Access-Control-Expose-Headers](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-expose-headers-response-header) +-- +-- @since 0.12.6 +hAccessControlExposeHeaders :: HeaderName +hAccessControlExposeHeaders = "Access-Control-Expose-Headers" + +-- | [Access-Control-Max-Age](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-max-age-response-header) +-- +-- @since 0.12.6 +hAccessControlMaxAge :: HeaderName +hAccessControlMaxAge = "Access-Control-Max-Age" + +-- | [Access-Control-Request-Method](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-request-method-request-header) +-- +-- @since 0.12.6 +hAccessControlRequestMethod :: HeaderName +hAccessControlRequestMethod = "Access-Control-Request-Method" + -- | [Accept-Language](https://www.rfc-editor.org/rfc/rfc9110.html#name-accept-language) -- -- @since 0.7.0 @@ -421,17 +478,41 @@ hWWWAuthenticate = "WWW-Authenticate" hWarning :: HeaderName hWarning = "Warning" +-- | [Accept-Patch](https://www.rfc-editor.org/rfc/rfc5789.html#section-3.1) +-- +-- @since 0.12.6 +hAcceptPatch :: HeaderName +hAcceptPatch = "Accept-Patch" + +-- | [Alt-Svc](https://www.rfc-editor.org/rfc/rfc7838.html#section-3) +-- +-- @since 0.12.6 +hAltSvc :: HeaderName +hAltSvc = "Alt-Svc" + -- | [Content-Disposition](https://www.rfc-editor.org/rfc/rfc6266.html) -- -- @since 0.10 hContentDisposition :: HeaderName hContentDisposition = "Content-Disposition" --- | [MIME-Version](https://www.rfc-editor.org/rfc/rfc2616.html#section-19.4.1) +-- | [Content-Digest](https://www.rfc-editor.org/rfc/rfc9530.html#section-2) -- --- @since 0.10 -hMIMEVersion :: HeaderName -hMIMEVersion = "MIME-Version" +-- @since 0.12.6 +hContentDigest :: HeaderName +hContentDigest = "Content-Digest" + +-- | [Content-Security-Policy](https://www.w3.org/TR/CSP3/#csp-header) +-- +-- @since 0.12.6 +hContentSecurityPolicy :: HeaderName +hContentSecurityPolicy = "Content-Security-Policy" + +-- | [Content-Security-Policy-Report-Only](https://www.w3.org/TR/CSP3/#cspro-header) +-- +-- @since 0.12.6 +hContentSecurityPolicyReportOnly :: HeaderName +hContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" -- | [Cookie](https://www.rfc-editor.org/rfc/rfc6265.html#section-4.2) -- @@ -439,11 +520,23 @@ hMIMEVersion = "MIME-Version" hCookie :: HeaderName hCookie = "Cookie" --- | [Set-Cookie](https://www.rfc-editor.org/rfc/rfc6265.html#section-4.1) +-- | [Forwarded](https://www.rfc-editor.org/rfc/rfc7239.html) +-- +-- @since 0.12.6 +hForwarded :: HeaderName +hForwarded = "Forwarded" + +-- | [Link](https://www.rfc-editor.org/rfc/rfc8288.html#section-3) +-- +-- @since 0.12.6 +hLink :: HeaderName +hLink = "Link" + +-- | [MIME-Version](https://www.rfc-editor.org/rfc/rfc2616.html#section-19.4.1) -- -- @since 0.10 -hSetCookie :: HeaderName -hSetCookie = "Set-Cookie" +hMIMEVersion :: HeaderName +hMIMEVersion = "MIME-Version" -- | [Origin](https://www.rfc-editor.org/rfc/rfc6454.html#section-7) -- @@ -463,6 +556,18 @@ hPrefer = "Prefer" hPreferenceApplied :: HeaderName hPreferenceApplied = "Preference-Applied" +-- | [Set-Cookie](https://www.rfc-editor.org/rfc/rfc6265.html#section-4.1) +-- +-- @since 0.10 +hSetCookie :: HeaderName +hSetCookie = "Set-Cookie" + +-- | [Strict-Transport-Security](https://www.rfc-editor.org/rfc/rfc6797.html#section-6.1) +-- +-- @since 0.12.6 +hStrictTransportSecurity :: HeaderName +hStrictTransportSecurity = "Strict-Transport-Security" + -- | An individual byte range. Used in @Range@ request headers. -- This type and its accompanying functions are /NOT/ compatible with the -- @Content-Range@ response header. diff --git a/test/Network/HTTP/Types/HeaderSpec.hs b/test/Network/HTTP/Types/HeaderSpec.hs index 1f4f146..e8e1195 100644 --- a/test/Network/HTTP/Types/HeaderSpec.hs +++ b/test/Network/HTTP/Types/HeaderSpec.hs @@ -46,13 +46,23 @@ allHeaders = , (hAcceptCharset, "Accept-Charset") , (hAcceptEncoding, "Accept-Encoding") , (hAcceptLanguage, "Accept-Language") + , (hAcceptPatch, "Accept-Patch") , (hAcceptQuery, "Accept-Query") , (hAcceptRanges, "Accept-Ranges") + , (hAccessControlAllowCredentials, "Access-Control-Allow-Credentials") + , (hAccessControlAllowHeaders, "Access-Control-Allow-Headers") + , (hAccessControlAllowMethods, "Access-Control-Allow-Methods") + , (hAccessControlAllowOrigin, "Access-Control-Allow-Origin") + , (hAccessControlExposeHeaders, "Access-Control-Expose-Headers") + , (hAccessControlMaxAge, "Access-Control-Max-Age") + , (hAccessControlRequestMethod, "Access-Control-Request-Method") , (hAge, "Age") , (hAllow, "Allow") + , (hAltSvc, "Alt-Svc") , (hAuthorization, "Authorization") , (hCacheControl, "Cache-Control") , (hConnection, "Connection") + , (hContentDigest, "Content-Digest") , (hContentDisposition, "Content-Disposition") , (hContentEncoding, "Content-Encoding") , (hContentLanguage, "Content-Language") @@ -60,12 +70,15 @@ allHeaders = , (hContentLocation, "Content-Location") , (hContentMD5, "Content-MD5") , (hContentRange, "Content-Range") + , (hContentSecurityPolicy, "Content-Security-Policy") + , (hContentSecurityPolicyReportOnly, "Content-Security-Policy-Report-Only") , (hContentType, "Content-Type") , (hCookie, "Cookie") , (hDate, "Date") , (hETag, "ETag") , (hExpect, "Expect") , (hExpires, "Expires") + , (hForwarded, "Forwarded") , (hFrom, "From") , (hHost, "Host") , (hIfMatch, "If-Match") @@ -74,6 +87,7 @@ allHeaders = , (hIfRange, "If-Range") , (hIfUnmodifiedSince, "If-Unmodified-Since") , (hLastModified, "Last-Modified") + , (hLink, "Link") , (hLocation, "Location") , (hMaxForwards, "Max-Forwards") , (hMIMEVersion, "MIME-Version") @@ -88,6 +102,7 @@ allHeaders = , (hRetryAfter, "Retry-After") , (hServer, "Server") , (hSetCookie, "Set-Cookie") + , (hStrictTransportSecurity, "Strict-Transport-Security") , (hTE, "TE") , (hTrailer, "Trailer") , (hTransferEncoding, "Transfer-Encoding") From 10d992ca3c09e599484caa942f3fa51be7738987 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 15:17:59 +0200 Subject: [PATCH 07/27] update CHANGELOG --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1227657..6a88afc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,28 @@ ## 0.12.6 [2026-08-13] * Remove `array` dependency +* Add pattern synonyms for `HttpVersion` + * `Http09` + * `Http10` + * `Http11` + * `Http20` + * `Http30` +* Add more constant headers: + * `hAcceptPatch` + * `hAccessControlAllowCredentials` + * `hAccessControlAllowHeaders` + * `hAccessControlAllowMethods` + * `hAccessControlAllowOrigin` + * `hAccessControlExposeHeaders` + * `hAccessControlMaxAge` + * `hAccessControlRequestMethod` + * `hAltSvc` + * `hContentDigest` + * `hContentSecurityPolicy` + * `hContentSecurityPolicyReportOnly` + * `hForwarded` + * `hLink` + * `hStrictTransportSecurity` ## 0.12.5 [2026-05-31] From dce09629ee72acb3f35fb382d0379319d178c4d4 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 16:15:24 +0200 Subject: [PATCH 08/27] add status pattern synonyms --- CHANGELOG.md | 2 + Network/HTTP/Types/Status.hs | 232 ++++++++++++++++++++++++++++++++++- 2 files changed, 233 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a88afc..05a6821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ * `Http11` * `Http20` * `Http30` +* Add pattern synonyms for `Status` + * all of format `StatusXXX` * Add more constant headers: * `hAcceptPatch` * `hAccessControlAllowCredentials` diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 24bbb19..a685bf4 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -1,13 +1,64 @@ {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE PatternSynonyms #-} -- | Types and constants to describe HTTP status codes. -- -- At the bottom are some functions to check if a given t'Status' is from a certain category. (i.e. @1XX@, @2XX@, etc.) module Network.HTTP.Types.Status ( -- * HTTP Status - Status (Status), + Status ( + Status, + Status100, + Status101, + Status200, + Status201, + Status202, + Status203, + Status204, + Status205, + Status206, + Status300, + Status301, + Status302, + Status303, + Status304, + Status305, + Status307, + Status308, + Status400, + Status401, + Status402, + Status403, + Status404, + Status405, + Status407, + Status408, + Status409, + Status410, + Status411, + Status412, + Status413, + Status414, + Status415, + Status416, + Status417, + Status418, + Status422, + Status426, + Status428, + Status429, + Status431, + Status451, + Status500, + Status501, + Status502, + Status503, + Status504, + Status505, + Status511 + ), statusCode, statusMessage, mkStatus, @@ -255,6 +306,14 @@ status101 = mkStatus 101 "Switching Protocols" switchingProtocols101 :: Status switchingProtocols101 = status101 +pattern Status100, Status101 :: Status + +-- | @since 0.12.6 +pattern Status100 <- Status 100 _ + +-- | @since 0.12.6 +pattern Status101 <- Status 101 _ + -- | OK 200 status200 :: Status status200 = mkStatus 200 "OK" @@ -331,6 +390,29 @@ status206 = mkStatus 206 "Partial Content" partialContent206 :: Status partialContent206 = status206 +pattern Status200, Status201, Status202, Status203, Status204, Status205, Status206 :: Status + +-- | @since 0.12.6 +pattern Status200 <- Status 200 _ + +-- | @since 0.12.6 +pattern Status201 <- Status 201 _ + +-- | @since 0.12.6 +pattern Status202 <- Status 202 _ + +-- | @since 0.12.6 +pattern Status203 <- Status 203 _ + +-- | @since 0.12.6 +pattern Status204 <- Status 204 _ + +-- | @since 0.12.6 +pattern Status205 <- Status 205 _ + +-- | @since 0.12.6 +pattern Status206 <- Status 206 _ + -- | Multiple Choices 300 status300 :: Status status300 = mkStatus 300 "Multiple Choices" @@ -411,6 +493,32 @@ status308 = mkStatus 308 "Permanent Redirect" permanentRedirect308 :: Status permanentRedirect308 = status308 +pattern Status300, Status301, Status302, Status303, Status304, Status305, Status307, Status308 :: Status + +-- | @since 0.12.6 +pattern Status300 <- Status 300 _ + +-- | @since 0.12.6 +pattern Status301 <- Status 301 _ + +-- | @since 0.12.6 +pattern Status302 <- Status 302 _ + +-- | @since 0.12.6 +pattern Status303 <- Status 303 _ + +-- | @since 0.12.6 +pattern Status304 <- Status 304 _ + +-- | @since 0.12.6 +pattern Status305 <- Status 305 _ + +-- | @since 0.12.6 +pattern Status307 <- Status 307 _ + +-- | @since 0.12.6 +pattern Status308 <- Status 308 _ + -- | Bad Request 400 status400 :: Status status400 = mkStatus 400 "Bad Request" @@ -706,6 +814,105 @@ status451 = mkStatus 451 "Unavailable For Legal Reasons" unavailableForLegalReasons451 :: Status unavailableForLegalReasons451 = status451 +pattern + Status400 + , Status401 + , Status402 + , Status403 + , Status404 + , Status405 + , Status407 + , Status408 + , Status409 + , Status410 + , Status411 + , Status412 + , Status413 + , Status414 + , Status415 + , Status416 + , Status417 + , Status418 + , Status422 + , Status426 + , Status428 + , Status429 + , Status431 + , Status451 :: + Status + +-- | @since 0.12.6 +pattern Status400 <- Status 400 _ + +-- | @since 0.12.6 +pattern Status401 <- Status 401 _ + +-- | @since 0.12.6 +pattern Status402 <- Status 402 _ + +-- | @since 0.12.6 +pattern Status403 <- Status 403 _ + +-- | @since 0.12.6 +pattern Status404 <- Status 404 _ + +-- | @since 0.12.6 +pattern Status405 <- Status 405 _ + +-- | @since 0.12.6 +pattern Status407 <- Status 407 _ + +-- | @since 0.12.6 +pattern Status408 <- Status 408 _ + +-- | @since 0.12.6 +pattern Status409 <- Status 409 _ + +-- | @since 0.12.6 +pattern Status410 <- Status 410 _ + +-- | @since 0.12.6 +pattern Status411 <- Status 411 _ + +-- | @since 0.12.6 +pattern Status412 <- Status 412 _ + +-- | @since 0.12.6 +pattern Status413 <- Status 413 _ + +-- | @since 0.12.6 +pattern Status414 <- Status 414 _ + +-- | @since 0.12.6 +pattern Status415 <- Status 415 _ + +-- | @since 0.12.6 +pattern Status416 <- Status 416 _ + +-- | @since 0.12.6 +pattern Status417 <- Status 417 _ + +-- | @since 0.12.6 +pattern Status418 <- Status 418 _ + +-- | @since 0.12.6 +pattern Status422 <- Status 422 _ + +-- | @since 0.12.6 +pattern Status426 <- Status 426 _ + +-- | @since 0.12.6 +pattern Status428 <- Status 428 _ + +-- | @since 0.12.6 +pattern Status429 <- Status 429 _ + +-- | @since 0.12.6 +pattern Status431 <- Status 431 _ + +-- | @since 0.12.6 +pattern Status451 <- Status 451 _ + -- | Internal Server Error 500 status500 :: Status status500 = mkStatus 500 "Internal Server Error" @@ -788,6 +995,29 @@ status511 = mkStatus 511 "Network Authentication Required" networkAuthenticationRequired511 :: Status networkAuthenticationRequired511 = status511 +pattern Status500, Status501, Status502, Status503, Status504, Status505, Status511 :: Status + +-- | @since 0.12.6 +pattern Status500 <- Status 500 _ + +-- | @since 0.12.6 +pattern Status501 <- Status 501 _ + +-- | @since 0.12.6 +pattern Status502 <- Status 502 _ + +-- | @since 0.12.6 +pattern Status503 <- Status 503 _ + +-- | @since 0.12.6 +pattern Status504 <- Status 504 _ + +-- | @since 0.12.6 +pattern Status505 <- Status 505 _ + +-- | @since 0.12.6 +pattern Status511 <- Status 511 _ + -- | Informational class -- -- Checks if the status is in the 1XX range. From b82eca7f8d62065081d705366ed7ed9c89135b61 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 16:19:45 +0200 Subject: [PATCH 09/27] add render functions for the 'Status' --- Network/HTTP/Types/Status.hs | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index a685bf4..3231a89 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -62,6 +62,11 @@ module Network.HTTP.Types.Status ( statusCode, statusMessage, mkStatus, + renderStatusCode, + renderFullStatus, + + -- ** Low level functions + renderStatusCodeToPtr, -- * Common statuses status100, @@ -171,9 +176,13 @@ module Network.HTTP.Types.Status ( statusIsServerError, ) where +import Data.Bits ((.&.)) import Data.ByteString as B (ByteString, empty) +import qualified Data.ByteString.Char8 as B8 +import Data.ByteString.Internal as B (ByteString (..), unsafeCreate, unsafeWithForeignPtr) import Data.Data (Data) import Data.Function (on) +import Foreign (Ptr, Word8, copyBytes, plusPtr, poke) import GHC.Generics (Generic) -- | HTTP Status. @@ -1057,3 +1066,43 @@ statusIsClientError (Status{statusCode = code}) = code >= 400 && code < 500 -- @since 0.8.0 statusIsServerError :: Status -> Bool statusIsServerError (Status{statusCode = code}) = code >= 500 && code < 600 + +-- | Write the 3 digit t'Status' code to the provided 'Ptr'. +-- +-- /N.B. This function assumes @statusCode < 1000@!/ +-- /If it is @> 1000@, the first byte will not be a digit./ +-- +-- @since 0.12.6 +renderStatusCodeToPtr :: Status -> Ptr Word8 -> IO () +renderStatusCodeToPtr (Status code _) ptr = do + poke ptr $ toByte h + poke (ptr `plusPtr` 1) $ toByte t + poke (ptr `plusPtr` 2) $ toByte i + where + (h, rest) = code `divMod` 100 + (t, i) = rest `divMod` 10 + toByte :: Int -> Word8 + toByte x = fromIntegral x .&. 0x30 + +-- | Render the 3 digit t'Status' code into a 'ByteString'. +-- +-- @since 0.12.6 +renderStatusCode :: Status -> ByteString +renderStatusCode s@(Status code _) + | code >= 1000 = B8.pack $ show s + | otherwise = + unsafeCreate 3 $ renderStatusCodeToPtr s + +-- | Render the full t'Status' code with status message into a 'ByteString'. +-- +-- @since 0.12.6 +renderFullStatus :: Status -> ByteString +renderFullStatus s@(Status code msg@(BS fptr len)) + | code >= 1000 = + B8.pack (show code) <> " " <> msg + | otherwise = + unsafeCreate (4 + len) $ \ptr -> do + renderStatusCodeToPtr s ptr + poke (ptr `plusPtr` 3) (0x20 :: Word8) + unsafeWithForeignPtr fptr $ \src -> + copyBytes ptr src len From 72ae6a79ebc2a250e3352a5beaa79b3c450890bb Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 16:42:30 +0200 Subject: [PATCH 10/27] test: unit tests for 'Status' rendering, and fixing found errors --- Network/HTTP/Types/Status.hs | 28 ++++++++++++++++++--------- test/Network/HTTP/Types/StatusSpec.hs | 13 +++++++++++++ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 3231a89..8ff5c94 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -176,8 +176,8 @@ module Network.HTTP.Types.Status ( statusIsServerError, ) where -import Data.Bits ((.&.)) -import Data.ByteString as B (ByteString, empty) +import Data.Bits ((.|.)) +import Data.ByteString as B (ByteString, empty, length) import qualified Data.ByteString.Char8 as B8 import Data.ByteString.Internal as B (ByteString (..), unsafeCreate, unsafeWithForeignPtr) import Data.Data (Data) @@ -1082,7 +1082,7 @@ renderStatusCodeToPtr (Status code _) ptr = do (h, rest) = code `divMod` 100 (t, i) = rest `divMod` 10 toByte :: Int -> Word8 - toByte x = fromIntegral x .&. 0x30 + toByte x = fromIntegral x .|. 0x30 -- | Render the 3 digit t'Status' code into a 'ByteString'. -- @@ -1093,16 +1093,26 @@ renderStatusCode s@(Status code _) | otherwise = unsafeCreate 3 $ renderStatusCodeToPtr s +-- | Writes the full t'Status' code to the provided 'Ptr'. +-- +-- /N.B. Same caveat as with 'renderStatusCodeToPtr'./ +-- +-- @since 0.12.6 +renderFullStatusToPtr :: Status -> Ptr Word8 -> IO () +renderFullStatusToPtr s@(Status _ (BS fptr len)) ptr = do + renderStatusCodeToPtr s ptr + poke (ptr `plusPtr` 3) (0x20 :: Word8) + unsafeWithForeignPtr fptr $ \src -> + copyBytes (ptr `plusPtr` 4) src len + -- | Render the full t'Status' code with status message into a 'ByteString'. -- -- @since 0.12.6 renderFullStatus :: Status -> ByteString -renderFullStatus s@(Status code msg@(BS fptr len)) +renderFullStatus s@(Status code msg) | code >= 1000 = B8.pack (show code) <> " " <> msg | otherwise = - unsafeCreate (4 + len) $ \ptr -> do - renderStatusCodeToPtr s ptr - poke (ptr `plusPtr` 3) (0x20 :: Word8) - unsafeWithForeignPtr fptr $ \src -> - copyBytes ptr src len + unsafeCreate (4 + len) $ renderFullStatusToPtr s + where + len = B.length msg diff --git a/test/Network/HTTP/Types/StatusSpec.hs b/test/Network/HTTP/Types/StatusSpec.hs index 7f8f700..516d19f 100644 --- a/test/Network/HTTP/Types/StatusSpec.hs +++ b/test/Network/HTTP/Types/StatusSpec.hs @@ -12,6 +12,7 @@ import Test.QuickCheck (Arbitrary (..), choose, property, resize) import Test.QuickCheck.Instances () import Network.HTTP.Types +import Network.HTTP.Types.Status main :: IO () main = hspec spec @@ -34,6 +35,18 @@ spec = do it "only orders on 'statusCode'" $ property $ \st1 st2 -> (st1 < st2) == ((<) `on` statusCode) st1 st2 + describe "Render functions" $ do + it "renders the code" $ do + renderStatusCode notFound404 `shouldBe` "404" + renderStatusCode continue100 `shouldBe` "100" + renderStatusCode (mkStatus 12 "") `shouldBe` "012" + renderStatusCode (mkStatus 987 "") `shouldBe` "987" + it "renders the message" $ do + renderFullStatus notFound404 `shouldBe` "404 Not Found" + renderFullStatus continue100 `shouldBe` "100 Continue" + renderFullStatus (mkStatus 12 "Short") `shouldBe` "012 Short" + renderFullStatus (mkStatus 987 "Pretty Long If I May Say So Myself") + `shouldBe` "987 Pretty Long If I May Say So Myself" categoryCheck :: String -> (Status -> Bool) -> [StatusTuple] -> Spec categoryCheck name p shoulds = do From d127ab9393f178e597a4dce4708bff2724fcb161 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 17:40:34 +0200 Subject: [PATCH 11/27] add parsing of status codes --- Network/HTTP/Types.hs | 14 +++++- Network/HTTP/Types/Status.hs | 94 ++++++++++++++++++++++++++++++++++-- 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/Network/HTTP/Types.hs b/Network/HTTP/Types.hs index 68f2258..040910e 100644 --- a/Network/HTTP/Types.hs +++ b/Network/HTTP/Types.hs @@ -36,9 +36,19 @@ module Network.HTTP.Types ( -- | __For more information__: "Network.HTTP.Types.Status" Status (..), + mkStatus, + + -- ** Parsing and Rendering + parseStatusCode, + renderStatusCode, + parseFullStatus, + renderFullStatus, + + -- ** Low level functions + renderStatusCodeToPtr, + renderFullStatusToPtr, -- ** Constants - mkStatus, status100, continue100, status101, @@ -137,6 +147,8 @@ module Network.HTTP.Types ( httpVersionNotSupported505, status511, networkAuthenticationRequired511, + + -- *** Category checks statusIsInformational, statusIsSuccessful, statusIsRedirection, diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 8ff5c94..8003e0d 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -62,11 +62,16 @@ module Network.HTTP.Types.Status ( statusCode, statusMessage, mkStatus, + + -- ** Parsing and Rendering + parseStatusCode, renderStatusCode, + parseFullStatus, renderFullStatus, -- ** Low level functions renderStatusCodeToPtr, + renderFullStatusToPtr, -- * Common statuses status100, @@ -176,13 +181,19 @@ module Network.HTTP.Types.Status ( statusIsServerError, ) where -import Data.Bits ((.|.)) -import Data.ByteString as B (ByteString, empty, length) +import Control.Monad (guard) +import Data.Bits ((.&.), (.|.)) +import Data.ByteString as B (ByteString, drop, empty, length, uncons) import qualified Data.ByteString.Char8 as B8 -import Data.ByteString.Internal as B (ByteString (..), unsafeCreate, unsafeWithForeignPtr) +import Data.ByteString.Internal as B ( + ByteString (..), + accursedUnutterablePerformIO, + unsafeCreate, + unsafeWithForeignPtr, + ) import Data.Data (Data) import Data.Function (on) -import Foreign (Ptr, Word8, copyBytes, plusPtr, poke) +import Foreign (Ptr, Word8, copyBytes, peek, plusPtr, poke) import GHC.Generics (Generic) -- | HTTP Status. @@ -1095,7 +1106,7 @@ renderStatusCode s@(Status code _) -- | Writes the full t'Status' code to the provided 'Ptr'. -- --- /N.B. Same caveat as with 'renderStatusCodeToPtr'./ +-- /N.B. Same caveat from 'renderStatusCodeToPtr' applies./ -- -- @since 0.12.6 renderFullStatusToPtr :: Status -> Ptr Word8 -> IO () @@ -1116,3 +1127,76 @@ renderFullStatus s@(Status code msg) unsafeCreate (4 + len) $ renderFullStatusToPtr s where len = B.length msg + +-- | Parses the first three characters as digits and converts them to an 'Int'. +-- +-- If the first 3 characters are not digits (i.e. @0-9@), or the 'ByteString' +-- is less than 3 bytes long, the result will be 'Nothing'. +-- +-- When successful, it will return the parsed status code and the remainder of +-- the 'ByteString'. +-- +-- >>> parseStatusCode "307" +-- Just (307,"") +-- +-- >>> parseStatusCode "404 Not Found" +-- Just (404," Not Found") +-- +-- >>> parseStatusCode "No Digits" +-- Nothing +-- +-- >>> parseStatusCode "12 Is Not Enough Digits" +-- Nothing +parseStatusCode :: ByteString -> Maybe (Int, ByteString) +parseStatusCode bs@(BS fptr len) + | len < 3 = Nothing + | otherwise = + accursedUnutterablePerformIO $ + unsafeWithForeignPtr fptr $ \ptr -> do + w1 <- peek ptr + w2 <- peek (ptr `plusPtr` 1) + w3 <- peek (ptr `plusPtr` 2) + pure $ do + h <- toNumber w1 + t <- toNumber w2 + i <- toNumber w3 + Just (h * 100 + t * 10 + i, B.drop 3 bs) + where + toNumber :: Word8 -> Maybe Int + toNumber w = do + guard $ 0x30 <= w && w <= 0x39 + Just . fromIntegral $ w .&. 0x0F + +-- | Assumes the provided 'ByteString' is either: +-- +-- * only 3 digits, or +-- * 3 digits, a space, and the rest of the status message +-- +-- /N.B. this function does not check for newlines, it puts everything/ +-- /after the code and space into the 'statusMessage'./ +-- +-- >>> parseFullStatus "307" +-- Just (Status {statusCode = 307, statusMessage = ""}) +-- +-- >>> parseFullStatus "404 Not Found" +-- Just (Status {statusCode = 404, statusMessage = "Not Found"}) +-- +-- >>> parseFullStatus "500 Someone Forgot To\r\nBreak At The Newline" +-- Just (Status {statusCode = 500, statusMessage = "Someone Forgot To\r\nBreak At The Newline"}) +-- +-- >>> parseFullStatus "1337 Is A Bad Status Code" +-- Nothing +-- +-- >>> parseFullStatus "101Still Needs A Space" +-- Nothing +-- +-- >>> parseFullStatus "No Digits" +-- Nothing +parseFullStatus :: ByteString -> Maybe Status +parseFullStatus bs = do + (code, rest) <- parseStatusCode bs + case B.uncons rest of + Nothing -> Just $ mkStatus code "" + Just (w, ws) + | w == 0x20 -> Just $ mkStatus code ws + | otherwise -> Nothing From c57bf86a5c9dff0bca6d7789984763f94794abfd Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 17:41:00 +0200 Subject: [PATCH 12/27] test: add testing of parsing of status codes --- test/Network/HTTP/Types/StatusSpec.hs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/test/Network/HTTP/Types/StatusSpec.hs b/test/Network/HTTP/Types/StatusSpec.hs index 516d19f..95f5ca3 100644 --- a/test/Network/HTTP/Types/StatusSpec.hs +++ b/test/Network/HTTP/Types/StatusSpec.hs @@ -47,6 +47,25 @@ spec = do renderFullStatus (mkStatus 12 "Short") `shouldBe` "012 Short" renderFullStatus (mkStatus 987 "Pretty Long If I May Say So Myself") `shouldBe` "987 Pretty Long If I May Say So Myself" + describe "Parsing functions" $ do + it "parses the code" $ do + parseStatusCode "307" `shouldBe` Just (307, "") + parseStatusCode "404 Not Found" `shouldBe` Just (404, " Not Found") + parseStatusCode "1337 Works Still" `shouldBe` Just (133, "7 Works Still") + parseStatusCode "No Digits" `shouldBe` Nothing + parseStatusCode "12 Is Not Enough" `shouldBe` Nothing + it "parses the message" $ do + parseFullStatus "307" `shouldBe` Just (mkStatus 307 "") + parseFullStatus "404 Not Found" `shouldBe` Just (mkStatus 404 "Not Found") + parseFullStatus "500 Someone Forgot To\r\nBreak At The Newline" + `shouldBe` Just (mkStatus 500 "Someone Forgot To\r\nBreak At The Newline") + parseFullStatus "1337 Works Still" `shouldBe` Nothing + parseFullStatus "No Digits" `shouldBe` Nothing + it "round trips" $ do + parseFullStatus (renderFullStatus notFound404) `shouldBe` Just notFound404 + -- I know this is under "round trips" and loses the message, but + -- it's about the status code here. + parseStatusCode (renderStatusCode notFound404) `shouldBe` Just (404, "") categoryCheck :: String -> (Status -> Bool) -> [StatusTuple] -> Spec categoryCheck name p shoulds = do From 8a063399987a708b0ace415004b4c23b384d8eba Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 18:09:06 +0200 Subject: [PATCH 13/27] CHANGELOG entry about parsing/rendering status codes, and fixing the 'HttpVersion' export for GHC 7.10.3 --- CHANGELOG.md | 1 + Network/HTTP/Types/Version.hs | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 05a6821..06424db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ## 0.12.6 [2026-08-13] * Remove `array` dependency +* Add parsing and rendering functions for `Status` * Add pattern synonyms for `HttpVersion` * `Http09` * `Http10` diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index 81131cc..a2fde9e 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -13,7 +13,9 @@ -- which would be @http/1.1@, @h2@ or @h3@. module Network.HTTP.Types.Version ( HttpVersion ( - .., + HttpVersion, + httpMajor, + httpMinor, Http09, Http10, Http11, From 8210aa8d82ad509d992f0f87917159bd32e952ba Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 18:15:46 +0200 Subject: [PATCH 14/27] fixes to build with GHC 7.10.3 --- Network/HTTP/Types/Version.hs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index a2fde9e..b5aa7a1 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -13,6 +13,7 @@ -- which would be @http/1.1@, @h2@ or @h3@. module Network.HTTP.Types.Version ( HttpVersion ( + -- DO NOT use "..," as GHC 7.10.3 doesn't parse that HttpVersion, httpMajor, httpMinor, @@ -86,7 +87,12 @@ http30 = HttpVersion 3 0 -- Pattern Synonyms -- ---------------------- -pattern Http09, Http10, Http11, Http20, Http30 :: HttpVersion +-- DO NOT put these on one line with commas, as GHC 7.10.3 doesn't parse that +pattern Http09 :: HttpVersion +pattern Http10 :: HttpVersion +pattern Http11 :: HttpVersion +pattern Http20 :: HttpVersion +pattern Http30 :: HttpVersion -- | @since 0.12.6 pattern Http09 = HttpVersion 0 9 From 7c06add099f012722ff74dd17dd85a18d891c0f4 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 23:28:16 +0200 Subject: [PATCH 15/27] hopefully fix building with GHC 7.10.3 --- Network/HTTP/Types/Status.hs | 135 +++++++++++++++++++++++++--------- Network/HTTP/Types/Version.hs | 15 +++- 2 files changed, 112 insertions(+), 38 deletions(-) diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 8003e0d..41f1cf2 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} @@ -8,6 +9,7 @@ -- At the bottom are some functions to check if a given t'Status' is from a certain category. (i.e. @1XX@, @2XX@, etc.) module Network.HTTP.Types.Status ( -- * HTTP Status +#if __GLASGOW_HASKELL__ >= 800 Status ( Status, Status100, @@ -59,6 +61,57 @@ module Network.HTTP.Types.Status ( Status505, Status511 ), +#else + Status(Status), + pattern Status100, + pattern Status101, + pattern Status200, + pattern Status201, + pattern Status202, + pattern Status203, + pattern Status204, + pattern Status205, + pattern Status206, + pattern Status300, + pattern Status301, + pattern Status302, + pattern Status303, + pattern Status304, + pattern Status305, + pattern Status307, + pattern Status308, + pattern Status400, + pattern Status401, + pattern Status402, + pattern Status403, + pattern Status404, + pattern Status405, + pattern Status407, + pattern Status408, + pattern Status409, + pattern Status410, + pattern Status411, + pattern Status412, + pattern Status413, + pattern Status414, + pattern Status415, + pattern Status416, + pattern Status417, + pattern Status418, + pattern Status422, + pattern Status426, + pattern Status428, + pattern Status429, + pattern Status431, + pattern Status451, + pattern Status500, + pattern Status501, + pattern Status502, + pattern Status503, + pattern Status504, + pattern Status505, + pattern Status511, +#endif statusCode, statusMessage, mkStatus, @@ -326,12 +379,12 @@ status101 = mkStatus 101 "Switching Protocols" switchingProtocols101 :: Status switchingProtocols101 = status101 -pattern Status100, Status101 :: Status - -- | @since 0.12.6 +pattern Status100 :: Status pattern Status100 <- Status 100 _ -- | @since 0.12.6 +pattern Status101 :: Status pattern Status101 <- Status 101 _ -- | OK 200 @@ -410,27 +463,32 @@ status206 = mkStatus 206 "Partial Content" partialContent206 :: Status partialContent206 = status206 -pattern Status200, Status201, Status202, Status203, Status204, Status205, Status206 :: Status - -- | @since 0.12.6 +pattern Status200 :: Status pattern Status200 <- Status 200 _ -- | @since 0.12.6 +pattern Status201 :: Status pattern Status201 <- Status 201 _ -- | @since 0.12.6 +pattern Status202 :: Status pattern Status202 <- Status 202 _ -- | @since 0.12.6 +pattern Status203 :: Status pattern Status203 <- Status 203 _ -- | @since 0.12.6 +pattern Status204 :: Status pattern Status204 <- Status 204 _ -- | @since 0.12.6 +pattern Status205 :: Status pattern Status205 <- Status 205 _ -- | @since 0.12.6 +pattern Status206 :: Status pattern Status206 <- Status 206 _ -- | Multiple Choices 300 @@ -513,30 +571,37 @@ status308 = mkStatus 308 "Permanent Redirect" permanentRedirect308 :: Status permanentRedirect308 = status308 -pattern Status300, Status301, Status302, Status303, Status304, Status305, Status307, Status308 :: Status -- | @since 0.12.6 +pattern Status300 :: Status pattern Status300 <- Status 300 _ -- | @since 0.12.6 +pattern Status301 :: Status pattern Status301 <- Status 301 _ -- | @since 0.12.6 +pattern Status302 :: Status pattern Status302 <- Status 302 _ -- | @since 0.12.6 +pattern Status303 :: Status pattern Status303 <- Status 303 _ -- | @since 0.12.6 +pattern Status304 :: Status pattern Status304 <- Status 304 _ -- | @since 0.12.6 +pattern Status305 :: Status pattern Status305 <- Status 305 _ -- | @since 0.12.6 +pattern Status307 :: Status pattern Status307 <- Status 307 _ -- | @since 0.12.6 +pattern Status308 :: Status pattern Status308 <- Status 308 _ -- | Bad Request 400 @@ -834,103 +899,100 @@ status451 = mkStatus 451 "Unavailable For Legal Reasons" unavailableForLegalReasons451 :: Status unavailableForLegalReasons451 = status451 -pattern - Status400 - , Status401 - , Status402 - , Status403 - , Status404 - , Status405 - , Status407 - , Status408 - , Status409 - , Status410 - , Status411 - , Status412 - , Status413 - , Status414 - , Status415 - , Status416 - , Status417 - , Status418 - , Status422 - , Status426 - , Status428 - , Status429 - , Status431 - , Status451 :: - Status - -- | @since 0.12.6 +pattern Status400 :: Status pattern Status400 <- Status 400 _ -- | @since 0.12.6 +pattern Status401 :: Status pattern Status401 <- Status 401 _ -- | @since 0.12.6 +pattern Status402 :: Status pattern Status402 <- Status 402 _ -- | @since 0.12.6 +pattern Status403 :: Status pattern Status403 <- Status 403 _ -- | @since 0.12.6 +pattern Status404 :: Status pattern Status404 <- Status 404 _ -- | @since 0.12.6 +pattern Status405 :: Status pattern Status405 <- Status 405 _ -- | @since 0.12.6 +pattern Status407 :: Status pattern Status407 <- Status 407 _ -- | @since 0.12.6 +pattern Status408 :: Status pattern Status408 <- Status 408 _ -- | @since 0.12.6 +pattern Status409 :: Status pattern Status409 <- Status 409 _ -- | @since 0.12.6 +pattern Status410 :: Status pattern Status410 <- Status 410 _ -- | @since 0.12.6 +pattern Status411 :: Status pattern Status411 <- Status 411 _ -- | @since 0.12.6 +pattern Status412 :: Status pattern Status412 <- Status 412 _ -- | @since 0.12.6 +pattern Status413 :: Status pattern Status413 <- Status 413 _ -- | @since 0.12.6 +pattern Status414 :: Status pattern Status414 <- Status 414 _ -- | @since 0.12.6 +pattern Status415 :: Status pattern Status415 <- Status 415 _ -- | @since 0.12.6 +pattern Status416 :: Status pattern Status416 <- Status 416 _ -- | @since 0.12.6 +pattern Status417 :: Status pattern Status417 <- Status 417 _ -- | @since 0.12.6 +pattern Status418 :: Status pattern Status418 <- Status 418 _ -- | @since 0.12.6 +pattern Status422 :: Status pattern Status422 <- Status 422 _ -- | @since 0.12.6 +pattern Status426 :: Status pattern Status426 <- Status 426 _ -- | @since 0.12.6 +pattern Status428 :: Status pattern Status428 <- Status 428 _ -- | @since 0.12.6 +pattern Status429 :: Status pattern Status429 <- Status 429 _ -- | @since 0.12.6 +pattern Status431 :: Status pattern Status431 <- Status 431 _ -- | @since 0.12.6 +pattern Status451 :: Status pattern Status451 <- Status 451 _ -- | Internal Server Error 500 @@ -1015,27 +1077,32 @@ status511 = mkStatus 511 "Network Authentication Required" networkAuthenticationRequired511 :: Status networkAuthenticationRequired511 = status511 -pattern Status500, Status501, Status502, Status503, Status504, Status505, Status511 :: Status - -- | @since 0.12.6 +pattern Status500 :: Status pattern Status500 <- Status 500 _ -- | @since 0.12.6 +pattern Status501 :: Status pattern Status501 <- Status 501 _ -- | @since 0.12.6 +pattern Status502 :: Status pattern Status502 <- Status 502 _ -- | @since 0.12.6 +pattern Status503 :: Status pattern Status503 <- Status 503 _ -- | @since 0.12.6 +pattern Status504 :: Status pattern Status504 <- Status 504 _ -- | @since 0.12.6 +pattern Status505 :: Status pattern Status505 <- Status 505 _ -- | @since 0.12.6 +pattern Status511 :: Status pattern Status511 <- Status 511 _ -- | Informational class diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index b5aa7a1..78ddeaf 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE CPP #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE PatternSynonyms #-} @@ -12,17 +13,23 @@ -- @HTTP/1.0@. If you're expecting HTTP v2 or v3, you'd be using ALPN tokens, -- which would be @http/1.1@, @h2@ or @h3@. module Network.HTTP.Types.Version ( +#if __GLASGOW_HASKELL__ >= 800 HttpVersion ( - -- DO NOT use "..," as GHC 7.10.3 doesn't parse that - HttpVersion, - httpMajor, - httpMinor, + .., Http09, Http10, Http11, Http20, Http30 ), +#else + HttpVersion (..), + pattern Http09, + pattern Http10, + pattern Http11, + pattern Http20, + pattern Http30, +#endif http09, http10, http11, From d9f93bcee15dea71a5aca9d4f07b8a8395895994 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Mon, 29 Jun 2026 23:55:00 +0200 Subject: [PATCH 16/27] unsafeWithForeignPtr fix --- Network/HTTP/Types/Status.hs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 41f1cf2..37a8803 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -242,11 +242,15 @@ import Data.ByteString.Internal as B ( ByteString (..), accursedUnutterablePerformIO, unsafeCreate, - unsafeWithForeignPtr, ) import Data.Data (Data) import Data.Function (on) import Foreign (Ptr, Word8, copyBytes, peek, plusPtr, poke) +#if !MIN_VERSION_base(4,15,0) +import Foreign.ForeignPtr (withForeignPtr) +#else +import GHC.ForeignPtr (unsafeWithForeignPtr) +#endif import GHC.Generics (Generic) -- | HTTP Status. @@ -1267,3 +1271,8 @@ parseFullStatus bs = do Just (w, ws) | w == 0x20 -> Just $ mkStatus code ws | otherwise -> Nothing + +#if !MIN_VERSION_base(4,15,0) +unsafeWithForeignPtr :: ForeignPtr a -> (Ptr a -> IO b) -> IO b +unsafeWithForeignPtr = withForeignPtr +#endif From 301c0ed89546ef92e795c9c6f0776f485e6ba3de Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 30 Jun 2026 00:11:36 +0200 Subject: [PATCH 17/27] more 7.10.3 fixes --- Network/HTTP/Types/Status.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 37a8803..eb2421a 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -1193,7 +1193,7 @@ renderFullStatusToPtr s@(Status _ (BS fptr len)) ptr = do renderFullStatus :: Status -> ByteString renderFullStatus s@(Status code msg) | code >= 1000 = - B8.pack (show code) <> " " <> msg + B8.pack (show code) `mappend` " " `mappend` msg | otherwise = unsafeCreate (4 + len) $ renderFullStatusToPtr s where From 218bdd661db0bd18e3dbc6d077334e8059ea2d22 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 30 Jun 2026 00:17:09 +0200 Subject: [PATCH 18/27] more fixes --- Network/HTTP/Types/Status.hs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index eb2421a..bf1bb58 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -247,7 +247,7 @@ import Data.Data (Data) import Data.Function (on) import Foreign (Ptr, Word8, copyBytes, peek, plusPtr, poke) #if !MIN_VERSION_base(4,15,0) -import Foreign.ForeignPtr (withForeignPtr) +import Foreign.ForeignPtr (ForeignPtr, withForeignPtr) #else import GHC.ForeignPtr (unsafeWithForeignPtr) #endif @@ -1181,11 +1181,11 @@ renderStatusCode s@(Status code _) -- -- @since 0.12.6 renderFullStatusToPtr :: Status -> Ptr Word8 -> IO () -renderFullStatusToPtr s@(Status _ (BS fptr len)) ptr = do +renderFullStatusToPtr s@(Status _ (PS fptr offset len)) ptr = do renderStatusCodeToPtr s ptr poke (ptr `plusPtr` 3) (0x20 :: Word8) unsafeWithForeignPtr fptr $ \src -> - copyBytes (ptr `plusPtr` 4) src len + copyBytes (ptr `plusPtr` 4) (src `plusPtr` offset) len -- | Render the full t'Status' code with status message into a 'ByteString'. -- @@ -1219,11 +1219,12 @@ renderFullStatus s@(Status code msg) -- >>> parseStatusCode "12 Is Not Enough Digits" -- Nothing parseStatusCode :: ByteString -> Maybe (Int, ByteString) -parseStatusCode bs@(BS fptr len) +parseStatusCode bs@(PS fptr offset len) | len < 3 = Nothing | otherwise = accursedUnutterablePerformIO $ - unsafeWithForeignPtr fptr $ \ptr -> do + unsafeWithForeignPtr fptr $ \ptr' -> do + let ptr = ptr' `plusPtr` offset w1 <- peek ptr w2 <- peek (ptr `plusPtr` 1) w3 <- peek (ptr `plusPtr` 2) From 0847f3051bdddc6405abd84d73ab4d59490c5d74 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Tue, 30 Jun 2026 01:21:11 +0200 Subject: [PATCH 19/27] test: add pattern match tests for the pattern synonyms --- test/Network/HTTP/Types/StatusSpec.hs | 57 +++++++++++++++++++++++++- test/Network/HTTP/Types/VersionSpec.hs | 16 +++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/test/Network/HTTP/Types/StatusSpec.hs b/test/Network/HTTP/Types/StatusSpec.hs index 95f5ca3..86fb681 100644 --- a/test/Network/HTTP/Types/StatusSpec.hs +++ b/test/Network/HTTP/Types/StatusSpec.hs @@ -1,4 +1,5 @@ {-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-orphans #-} module Network.HTTP.Types.StatusSpec (main, spec) where @@ -12,7 +13,6 @@ import Test.QuickCheck (Arbitrary (..), choose, property, resize) import Test.QuickCheck.Instances () import Network.HTTP.Types -import Network.HTTP.Types.Status main :: IO () main = hspec spec @@ -66,6 +66,61 @@ spec = do -- I know this is under "round trips" and loses the message, but -- it's about the status code here. parseStatusCode (renderStatusCode notFound404) `shouldBe` Just (404, "") + describe "Patterns" $ do + patternMatch status100 (\Status100 -> pure ()) + patternMatch status101 (\Status101 -> pure ()) + patternMatch status200 (\Status200 -> pure ()) + patternMatch status201 (\Status201 -> pure ()) + patternMatch status202 (\Status202 -> pure ()) + patternMatch status203 (\Status203 -> pure ()) + patternMatch status204 (\Status204 -> pure ()) + patternMatch status205 (\Status205 -> pure ()) + patternMatch status206 (\Status206 -> pure ()) + patternMatch status300 (\Status300 -> pure ()) + patternMatch status301 (\Status301 -> pure ()) + patternMatch status302 (\Status302 -> pure ()) + patternMatch status303 (\Status303 -> pure ()) + patternMatch status304 (\Status304 -> pure ()) + patternMatch status305 (\Status305 -> pure ()) + patternMatch status307 (\Status307 -> pure ()) + patternMatch status308 (\Status308 -> pure ()) + patternMatch status400 (\Status400 -> pure ()) + patternMatch status401 (\Status401 -> pure ()) + patternMatch status402 (\Status402 -> pure ()) + patternMatch status403 (\Status403 -> pure ()) + patternMatch status404 (\Status404 -> pure ()) + patternMatch status405 (\Status405 -> pure ()) + patternMatch status407 (\Status407 -> pure ()) + patternMatch status408 (\Status408 -> pure ()) + patternMatch status409 (\Status409 -> pure ()) + patternMatch status410 (\Status410 -> pure ()) + patternMatch status411 (\Status411 -> pure ()) + patternMatch status412 (\Status412 -> pure ()) + patternMatch status413 (\Status413 -> pure ()) + patternMatch status414 (\Status414 -> pure ()) + patternMatch status415 (\Status415 -> pure ()) + patternMatch status416 (\Status416 -> pure ()) + patternMatch status417 (\Status417 -> pure ()) + patternMatch status418 (\Status418 -> pure ()) + patternMatch status422 (\Status422 -> pure ()) + patternMatch status426 (\Status426 -> pure ()) + patternMatch status428 (\Status428 -> pure ()) + patternMatch status429 (\Status429 -> pure ()) + patternMatch status431 (\Status431 -> pure ()) + patternMatch status451 (\Status451 -> pure ()) + patternMatch status500 (\Status500 -> pure ()) + patternMatch status501 (\Status501 -> pure ()) + patternMatch status502 (\Status502 -> pure ()) + patternMatch status503 (\Status503 -> pure ()) + patternMatch status504 (\Status504 -> pure ()) + patternMatch status505 (\Status505 -> pure ()) + patternMatch status511 (\Status511 -> pure ()) + +patternMatch :: Status -> (Status -> IO ()) -> SpecWith () +patternMatch s f = + it name $ f s `shouldReturn` () + where + name = "match the status " <> show (statusCode s) categoryCheck :: String -> (Status -> Bool) -> [StatusTuple] -> Spec categoryCheck name p shoulds = do diff --git a/test/Network/HTTP/Types/VersionSpec.hs b/test/Network/HTTP/Types/VersionSpec.hs index 1a80923..e5ef90d 100644 --- a/test/Network/HTTP/Types/VersionSpec.hs +++ b/test/Network/HTTP/Types/VersionSpec.hs @@ -1,3 +1,5 @@ +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + module Network.HTTP.Types.VersionSpec (main, spec) where import Test.Hspec @@ -8,9 +10,21 @@ main :: IO () main = hspec spec spec :: Spec -spec = +spec = do describe "Regression tests" $ mapM_ checkVersion allVersions + describe "Patterns" $ do + patternMatch http09 (\Http09 -> pure ()) + patternMatch http10 (\Http10 -> pure ()) + patternMatch http11 (\Http11 -> pure ()) + patternMatch http20 (\Http20 -> pure ()) + patternMatch http30 (\Http30 -> pure ()) + +patternMatch :: HttpVersion -> (HttpVersion -> IO ()) -> SpecWith () +patternMatch hv f = + it name $ f hv `shouldReturn` () + where + name = "match the version " <> show hv -- | [("Rendered", {constant}, {literal})] allVersions :: [(String, HttpVersion, HttpVersion)] From c78e02e7e6f669281d379dbcb59838764da601de Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Thu, 2 Jul 2026 17:16:09 +0200 Subject: [PATCH 20/27] miscellaneous adjustments * use more hex notation * more literal manipulating of bytes * some documentation * add name to copyright holders --- Network/HTTP/Types/Status.hs | 4 +++- Network/HTTP/Types/URI.hs | 40 ++++++++++++++++++++++-------------- http-types.cabal | 2 +- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index bf1bb58..0abf1c7 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -117,6 +117,8 @@ module Network.HTTP.Types.Status ( mkStatus, -- ** Parsing and Rendering + + -- | These functions are quicker and more efficient than doing it yourself. parseStatusCode, renderStatusCode, parseFullStatus, @@ -1152,7 +1154,7 @@ statusIsServerError (Status{statusCode = code}) = code >= 500 && code < 600 -- | Write the 3 digit t'Status' code to the provided 'Ptr'. -- -- /N.B. This function assumes @statusCode < 1000@!/ --- /If it is @> 1000@, the first byte will not be a digit./ +-- /If it is @>= 1000@, the first byte will not be a digit./ -- -- @since 0.12.6 renderStatusCodeToPtr :: Status -> Ptr Word8 -> IO () diff --git a/Network/HTTP/Types/URI.hs b/Network/HTTP/Types/URI.hs index d44ffc7..f920f19 100644 --- a/Network/HTTP/Types/URI.hs +++ b/Network/HTTP/Types/URI.hs @@ -299,11 +299,12 @@ urlEncodeBuilder' extraUnreserved = | otherwise = h2 ch -- The order is optimized from most expected to least expected - unreserved ch - | ch >= 0x61 && ch <= 0x7A = True -- a-z - | ch >= 0x30 && ch <= 0x39 = True -- 0-9 - | ch >= 0x41 && ch <= 0x5A = True -- A-Z - | otherwise = ch `elem` extraUnreserved + unreserved ch = + -- FIXME: could be one index lookup + (ch >= 0x61 && ch <= 0x7A) -- a-z + || (ch >= 0x30 && ch <= 0x39) -- 0-9 + || (ch >= 0x41 && ch <= 0x5A) -- A-Z + || (ch `elem` extraUnreserved) -- must be upper-case h2 v = B.word8 _percent `mappend` B.word8 (h a) `mappend` B.word8 (h b) @@ -311,8 +312,8 @@ urlEncodeBuilder' extraUnreserved = a = v `shiftR` 4 b = v .&. 0x0F h i - | i < 10 = 0x30 + i -- zero (0) - | otherwise = 0x41 + i - 10 -- 0x41: A + | i < 10 = 0x30 + i -- digits (0x30 == '0') + | otherwise = 0x37 + i -- A-F (0x41 - 10; 0x41 == 'A') -- | Percent-encoding for URLs. -- @@ -369,10 +370,13 @@ urlDecode replacePlus z = fst $ B.unfoldrN (B.length z) go z Just (a `combine` b, ys) Just other -> Just other hexVal w - | 0x30 <= w && w <= 0x39 = Just $ w .&. 0x0F -- 0 - 9 - | 0x41 <= w && w <= 0x46 = Just $ w - 0x37 -- A - F ((w - 0x41) + 10) - | 0x61 <= w && w <= 0x66 = Just $ w - 0x57 -- a - f ((w - 0x61) + 10) + -- FIXME: could be one index lookup + | 0x30 <= w && w <= 0x39 = Just result -- 0 - 9 + | 0x41 <= w && w <= 0x46 = Just (result + 9) -- A - F + | 0x61 <= w && w <= 0x66 = Just (result + 9) -- a - f | otherwise = Nothing + where + result = w .&. 0x0F combine :: Word8 -> Word8 -> Word8 combine a b = shiftL a 4 .|. b @@ -475,11 +479,17 @@ decodePathSegment = decodeUtf8With lenientDecode . urlDecode False extractPath :: B.ByteString -> B.ByteString extractPath = ensureNonEmpty . extract where - extract path - | "http://" `B.isPrefixOf` path = (snd . breakOnSlash . B.drop 7) path - | "https://" `B.isPrefixOf` path = (snd . breakOnSlash . B.drop 8) path - | otherwise = path - breakOnSlash = B.break (== _slash) + extract path = + case prefix of + "http://" -> fromSlash rest + "https:/" + -- we need one more _slash for it to be a correct protocol prefix + | Just (0x2F, more) <- B.uncons rest -> + fromSlash more + _ -> path + where + (prefix, rest) = B.splitAt 7 path + fromSlash = B.dropWhile (/= _slash) ensureNonEmpty "" = "/" ensureNonEmpty p = p diff --git a/http-types.cabal b/http-types.cabal index e8b2c1c..2055dcb 100644 --- a/http-types.cabal +++ b/http-types.cabal @@ -7,7 +7,7 @@ Description: Types and functions to describe and handle HTTP concepts. Homepage: https://github.com/Vlix/http-types License: BSD-3-Clause License-file: LICENSE -Author: Aristid Breitkreuz, Michael Snoyman +Author: Felix Paulusma, Aristid Breitkreuz, Michael Snoyman Maintainer: felix.paulusma@gmail.com Copyright: (C) 2011 Aristid Breitkreuz, (C) 2023 Felix Paulusma Category: Network, Web From 6c4c96f9062a3672e9af0372d0275619e7dc00a6 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Thu, 13 Aug 2026 15:28:43 +0200 Subject: [PATCH 21/27] bump version to 0.13.0 --- CHANGELOG.md | 7 +++++-- http-types.cabal | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 06424db..d1b8028 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,9 +5,8 @@ * Add support for the QUERY method and Accept-Query response field name from [RFC 10008](https://www.rfc-editor.org/rfc/rfc10008.html). -## 0.12.6 [2026-08-13] +## 0.12.7 [unreleased] -* Remove `array` dependency * Add parsing and rendering functions for `Status` * Add pattern synonyms for `HttpVersion` * `Http09` @@ -34,6 +33,10 @@ * `hLink` * `hStrictTransportSecurity` +## 0.12.6 [2026-08-13] + +* Remove `array` dependency + ## 0.12.5 [2026-05-31] * Add status `451 Unavailable For Legal Reasons` diff --git a/http-types.cabal b/http-types.cabal index 2055dcb..b974c14 100644 --- a/http-types.cabal +++ b/http-types.cabal @@ -1,6 +1,6 @@ Cabal-version: 2.2 Name: http-types -Version: 0.12.6 +Version: 0.13.0 Synopsis: Generic HTTP types for Haskell (for both client and server code). Description: Types and functions to describe and handle HTTP concepts. Including "methods", "headers", "query strings", "paths" and "HTTP versions". @@ -24,7 +24,7 @@ Extra-doc-files: Source-repository this type: git location: https://github.com/Vlix/http-types.git - tag: v0.12.6 + tag: v0.13.0 Source-repository head type: git From 1be5723cc7290bdc8c6c53031eb7a9d6363b8b18 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Thu, 13 Aug 2026 17:11:38 +0200 Subject: [PATCH 22/27] haddock should display 'since 0.13.0' --- Network/HTTP/Types/Header.hs | 30 +++++----- Network/HTTP/Types/Status.hs | 104 +++++++++++++++++----------------- Network/HTTP/Types/Version.hs | 10 ++-- 3 files changed, 72 insertions(+), 72 deletions(-) diff --git a/Network/HTTP/Types/Header.hs b/Network/HTTP/Types/Header.hs index fcfb755..72296d0 100644 --- a/Network/HTTP/Types/Header.hs +++ b/Network/HTTP/Types/Header.hs @@ -162,43 +162,43 @@ hAcceptEncoding = "Accept-Encoding" -- | [Access-Control-Allow-Credentials](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-origin-response-header) -- --- @since 0.12.6 +-- @since 0.13.0 hAccessControlAllowCredentials :: HeaderName hAccessControlAllowCredentials = "Access-Control-Allow-Credentials" -- | [Access-Control-Allow-Headers](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-headers-response-header) -- --- @since 0.12.6 +-- @since 0.13.0 hAccessControlAllowHeaders :: HeaderName hAccessControlAllowHeaders = "Access-Control-Allow-Headers" -- | [Access-Control-Allow-Methods](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-methods-response-header) -- --- @since 0.12.6 +-- @since 0.13.0 hAccessControlAllowMethods :: HeaderName hAccessControlAllowMethods = "Access-Control-Allow-Methods" -- | [Access-Control-Allow-Origin](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-origin-response-header) -- --- @since 0.12.6 +-- @since 0.13.0 hAccessControlAllowOrigin :: HeaderName hAccessControlAllowOrigin = "Access-Control-Allow-Origin" -- | [Access-Control-Expose-Headers](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-expose-headers-response-header) -- --- @since 0.12.6 +-- @since 0.13.0 hAccessControlExposeHeaders :: HeaderName hAccessControlExposeHeaders = "Access-Control-Expose-Headers" -- | [Access-Control-Max-Age](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-max-age-response-header) -- --- @since 0.12.6 +-- @since 0.13.0 hAccessControlMaxAge :: HeaderName hAccessControlMaxAge = "Access-Control-Max-Age" -- | [Access-Control-Request-Method](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-request-method-request-header) -- --- @since 0.12.6 +-- @since 0.13.0 hAccessControlRequestMethod :: HeaderName hAccessControlRequestMethod = "Access-Control-Request-Method" @@ -480,13 +480,13 @@ hWarning = "Warning" -- | [Accept-Patch](https://www.rfc-editor.org/rfc/rfc5789.html#section-3.1) -- --- @since 0.12.6 +-- @since 0.13.0 hAcceptPatch :: HeaderName hAcceptPatch = "Accept-Patch" -- | [Alt-Svc](https://www.rfc-editor.org/rfc/rfc7838.html#section-3) -- --- @since 0.12.6 +-- @since 0.13.0 hAltSvc :: HeaderName hAltSvc = "Alt-Svc" @@ -498,19 +498,19 @@ hContentDisposition = "Content-Disposition" -- | [Content-Digest](https://www.rfc-editor.org/rfc/rfc9530.html#section-2) -- --- @since 0.12.6 +-- @since 0.13.0 hContentDigest :: HeaderName hContentDigest = "Content-Digest" -- | [Content-Security-Policy](https://www.w3.org/TR/CSP3/#csp-header) -- --- @since 0.12.6 +-- @since 0.13.0 hContentSecurityPolicy :: HeaderName hContentSecurityPolicy = "Content-Security-Policy" -- | [Content-Security-Policy-Report-Only](https://www.w3.org/TR/CSP3/#cspro-header) -- --- @since 0.12.6 +-- @since 0.13.0 hContentSecurityPolicyReportOnly :: HeaderName hContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" @@ -522,13 +522,13 @@ hCookie = "Cookie" -- | [Forwarded](https://www.rfc-editor.org/rfc/rfc7239.html) -- --- @since 0.12.6 +-- @since 0.13.0 hForwarded :: HeaderName hForwarded = "Forwarded" -- | [Link](https://www.rfc-editor.org/rfc/rfc8288.html#section-3) -- --- @since 0.12.6 +-- @since 0.13.0 hLink :: HeaderName hLink = "Link" @@ -564,7 +564,7 @@ hSetCookie = "Set-Cookie" -- | [Strict-Transport-Security](https://www.rfc-editor.org/rfc/rfc6797.html#section-6.1) -- --- @since 0.12.6 +-- @since 0.13.0 hStrictTransportSecurity :: HeaderName hStrictTransportSecurity = "Strict-Transport-Security" diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 0abf1c7..4365a5c 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -385,11 +385,11 @@ status101 = mkStatus 101 "Switching Protocols" switchingProtocols101 :: Status switchingProtocols101 = status101 --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status100 :: Status pattern Status100 <- Status 100 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status101 :: Status pattern Status101 <- Status 101 _ @@ -469,31 +469,31 @@ status206 = mkStatus 206 "Partial Content" partialContent206 :: Status partialContent206 = status206 --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status200 :: Status pattern Status200 <- Status 200 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status201 :: Status pattern Status201 <- Status 201 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status202 :: Status pattern Status202 <- Status 202 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status203 :: Status pattern Status203 <- Status 203 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status204 :: Status pattern Status204 <- Status 204 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status205 :: Status pattern Status205 <- Status 205 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status206 :: Status pattern Status206 <- Status 206 _ @@ -578,35 +578,35 @@ permanentRedirect308 :: Status permanentRedirect308 = status308 --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status300 :: Status pattern Status300 <- Status 300 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status301 :: Status pattern Status301 <- Status 301 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status302 :: Status pattern Status302 <- Status 302 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status303 :: Status pattern Status303 <- Status 303 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status304 :: Status pattern Status304 <- Status 304 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status305 :: Status pattern Status305 <- Status 305 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status307 :: Status pattern Status307 <- Status 307 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status308 :: Status pattern Status308 <- Status 308 _ @@ -905,99 +905,99 @@ status451 = mkStatus 451 "Unavailable For Legal Reasons" unavailableForLegalReasons451 :: Status unavailableForLegalReasons451 = status451 --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status400 :: Status pattern Status400 <- Status 400 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status401 :: Status pattern Status401 <- Status 401 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status402 :: Status pattern Status402 <- Status 402 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status403 :: Status pattern Status403 <- Status 403 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status404 :: Status pattern Status404 <- Status 404 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status405 :: Status pattern Status405 <- Status 405 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status407 :: Status pattern Status407 <- Status 407 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status408 :: Status pattern Status408 <- Status 408 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status409 :: Status pattern Status409 <- Status 409 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status410 :: Status pattern Status410 <- Status 410 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status411 :: Status pattern Status411 <- Status 411 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status412 :: Status pattern Status412 <- Status 412 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status413 :: Status pattern Status413 <- Status 413 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status414 :: Status pattern Status414 <- Status 414 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status415 :: Status pattern Status415 <- Status 415 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status416 :: Status pattern Status416 <- Status 416 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status417 :: Status pattern Status417 <- Status 417 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status418 :: Status pattern Status418 <- Status 418 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status422 :: Status pattern Status422 <- Status 422 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status426 :: Status pattern Status426 <- Status 426 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status428 :: Status pattern Status428 <- Status 428 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status429 :: Status pattern Status429 <- Status 429 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status431 :: Status pattern Status431 <- Status 431 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status451 :: Status pattern Status451 <- Status 451 _ @@ -1083,31 +1083,31 @@ status511 = mkStatus 511 "Network Authentication Required" networkAuthenticationRequired511 :: Status networkAuthenticationRequired511 = status511 --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status500 :: Status pattern Status500 <- Status 500 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status501 :: Status pattern Status501 <- Status 501 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status502 :: Status pattern Status502 <- Status 502 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status503 :: Status pattern Status503 <- Status 503 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status504 :: Status pattern Status504 <- Status 504 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status505 :: Status pattern Status505 <- Status 505 _ --- | @since 0.12.6 +-- | @since 0.13.0 pattern Status511 :: Status pattern Status511 <- Status 511 _ @@ -1156,7 +1156,7 @@ statusIsServerError (Status{statusCode = code}) = code >= 500 && code < 600 -- /N.B. This function assumes @statusCode < 1000@!/ -- /If it is @>= 1000@, the first byte will not be a digit./ -- --- @since 0.12.6 +-- @since 0.13.0 renderStatusCodeToPtr :: Status -> Ptr Word8 -> IO () renderStatusCodeToPtr (Status code _) ptr = do poke ptr $ toByte h @@ -1170,7 +1170,7 @@ renderStatusCodeToPtr (Status code _) ptr = do -- | Render the 3 digit t'Status' code into a 'ByteString'. -- --- @since 0.12.6 +-- @since 0.13.0 renderStatusCode :: Status -> ByteString renderStatusCode s@(Status code _) | code >= 1000 = B8.pack $ show s @@ -1181,7 +1181,7 @@ renderStatusCode s@(Status code _) -- -- /N.B. Same caveat from 'renderStatusCodeToPtr' applies./ -- --- @since 0.12.6 +-- @since 0.13.0 renderFullStatusToPtr :: Status -> Ptr Word8 -> IO () renderFullStatusToPtr s@(Status _ (PS fptr offset len)) ptr = do renderStatusCodeToPtr s ptr @@ -1191,7 +1191,7 @@ renderFullStatusToPtr s@(Status _ (PS fptr offset len)) ptr = do -- | Render the full t'Status' code with status message into a 'ByteString'. -- --- @since 0.12.6 +-- @since 0.13.0 renderFullStatus :: Status -> ByteString renderFullStatus s@(Status code msg) | code >= 1000 = diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index 78ddeaf..66ddba6 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -101,17 +101,17 @@ pattern Http11 :: HttpVersion pattern Http20 :: HttpVersion pattern Http30 :: HttpVersion --- | @since 0.12.6 +-- | @since 0.13.0 pattern Http09 = HttpVersion 0 9 --- | @since 0.12.6 +-- | @since 0.13.0 pattern Http10 = HttpVersion 1 0 --- | @since 0.12.6 +-- | @since 0.13.0 pattern Http11 = HttpVersion 1 1 --- | @since 0.12.6 +-- | @since 0.13.0 pattern Http20 = HttpVersion 2 0 --- | @since 0.12.6 +-- | @since 0.13.0 pattern Http30 = HttpVersion 3 0 From 2afc1e97c859cdb9d9fd3d5614204642dcb1b4ef Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Thu, 13 Aug 2026 21:13:07 +0200 Subject: [PATCH 23/27] ci: better caching --- .github/workflows/ci.yml | 206 +++++++++++++++++++++++---------------- http-types.cabal | 2 +- 2 files changed, 122 insertions(+), 86 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 591bffb..0374ad8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,40 +8,17 @@ on: - reopened - synchronize paths: - - '**.hs' - - '**.cabal' - - 'stack.yaml' - - '.github/workflows/ci.yml' + - "**.hs" + - "**.cabal" + - "stack.yaml" + - ".github/workflows/ci.yml" push: branches: [master] jobs: - # Just to test very old compiler for low base version dependency - oldest: - name: oldest / ghc-7.10.3 / ubuntu - runs-on: ubuntu-latest - steps: - - - uses: actions/checkout@v4 - - - uses: haskell-actions/setup@v2 - id: setup-haskell-cabal - name: Setup Haskell - with: - ghc-version: "7.10.3" - cabal-version: 3.0.0.0 - - - uses: actions/cache@v3 - name: Cache cabal-store - with: - path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }} - key: ubuntu-7.10.3-cabal - - - name: Build - run: | - cabal build --ghc-option='-Wall' --project-file=cabal.7.10.3.project - cabal: + env: + MANUAL_RESET: v4 name: cabal / ghc-${{matrix.ghc}} / ${{ matrix.os }} continue-on-error: ${{ matrix.ghc == '9.14.1'}} runs-on: ${{ matrix.os }} @@ -49,7 +26,7 @@ jobs: matrix: os: - ubuntu-latest - - macOS-latest + # - macOS-latest # Is giving issues for some reason cabal: ["latest"] ghc: - "9.6.7" @@ -59,31 +36,43 @@ jobs: - "9.14.1" steps: - - uses: actions/checkout@v4 - - - uses: haskell-actions/setup@v2 - id: setup-haskell-cabal - name: Setup Haskell - with: - ghc-version: ${{ matrix.ghc }} - cabal-version: ${{ matrix.cabal }} - cabal-update: true - - - uses: actions/cache@v3 - name: Cache cabal-store - with: - path: ${{ steps.setup-haskell-cabal.outputs.cabal-store }} - key: ${{ runner.os }}-${{ matrix.ghc }}-cabal - - - name: Build - run: | - cabal build all --enable-tests --enable-benchmarks --write-ghc-environment-files=always --ghc-option='-Wall' - - name: Test - run: | - cabal test test:spec --enable-tests --ghc-option='-Wall' - - name: Test Docs - run: | - cabal exec -- cabal test test:doctests --enable-tests --ghc-option='-Wall' + - uses: actions/checkout@v4 + + - name: Cache GHCup + uses: actions/cache@v6 + id: ghcup-cache + with: + path: | + ~/.ghcup/bin/* + ~/.ghcup/cache/* + ~/.ghcup/config.yaml + ~/.ghcup/ghc/${{ matrix.ghc }} + key: CI-ghcup-cabal-${{ env.MANUAL_RESET }}-${{ matrix.ghc }} + + - name: Setup Haskell + if: steps.ghcup-cache.outputs.cache-hit != 'true' + uses: haskell-actions/setup@v2 + id: setup-haskell-cabal + with: + ghc-version: ${{ matrix.ghc }} + cabal-version: ${{ matrix.cabal }} + cabal-update: true + + - name: Cache cabal + uses: actions/cache@v6 + with: + path: ~/.cabal + key: ${{ matrix.ghc }}-cabal + + - name: Build + run: | + cabal build all --enable-tests --enable-benchmarks --write-ghc-environment-files=always --ghc-option='-Wall' + - name: Test + run: | + cabal test test:spec --enable-tests --ghc-option='-Wall' + - name: Test Docs + run: | + cabal exec -- cabal test test:doctests --enable-tests --ghc-option='-Wall' # # We probably want to add benchmarks at some point, just to make sure # # functions don't regress in performance too much? # - name: Bench @@ -91,6 +80,8 @@ jobs: # cabal bench --enable-benchmarks stack: name: stack ${{ matrix.resolver }} + env: + MANUAL_RESET: v3 runs-on: ubuntu-latest # This makes the CI jobs not all be cancelled if nightly fails to build. # However, if nightly fails to build, CI still gets a red X in the GitHub UI. @@ -100,40 +91,85 @@ jobs: # When some sort of `allow-failure` functionality is available in GitHub # actions, we should switch to it: # https://github.com/actions/toolkit/issues/399 - continue-on-error: ${{ matrix.resolver == '--resolver nightly' }} + continue-on-error: ${{ matrix.resolver == 'nightly' || matrix.resolver == 'lts-12' }} strategy: matrix: - stack: ["latest"] - resolver: - - "--resolver lts-22" # GHC 9.6.7 - - "--resolver lts-23" # GHC 9.8.4 - - "--resolver lts-24" # GHC 9.10.3 - - "--resolver nightly" # GHC 9.12.4 + include: + - resolver: "lts-12" + ghc: "8.4.4" + - resolver: "lts-22" + ghc: "9.6.7" + - resolver: "lts-23" + ghc: "9.8.4" + - resolver: "lts-24" + ghc: "9.10.3" + - resolver: "nightly" + ghc: "9.12.4" steps: - - uses: actions/checkout@v4 - - - uses: haskell-actions/setup@v2 - name: Setup Haskell Stack - with: - stack-version: ${{ matrix.stack }} - enable-stack: true - - - uses: actions/cache@v3 - name: Cache ~/.stack - with: - path: ${{ steps.setup-haskell-cabal.outputs.stack-root }} - key: ${{ runner.os }}-${{ matrix.resolver }}-stack - - - name: Build - run: | - stack build ${{ matrix.resolver }} --test --bench --no-run-tests --no-run-benchmarks --ghc-options='-Wall' - - name: Test - run: | - stack test --no-rerun-tests http-types:test:spec ${{ matrix.resolver }} - - name: Test Docs - run: | - stack test http-types:test:doctests ${{ matrix.resolver }} + - uses: actions/checkout@v4 + + - name: Cache GHCup + uses: actions/cache@v6 + id: ghcup-cache + with: + path: | + ~/.ghcup/bin/* + ~/.ghcup/cache/* + ~/.ghcup/config.yaml + ~/.ghcup/ghc/${{ matrix.ghc }} + ~/.ghcup/stack/* + key: CI-ghcup-stack-${{ env.MANUAL_RESET }}-${{ matrix.ghc }} + + - name: Setup Haskell Stack + uses: haskell-actions/setup@v2 + if: steps.ghcup-cache.outputs.cache-hit != 'true' + with: + stack-version: "latest" + ghc-version: ${{ matrix.ghc }} + enable-stack: true + stack-setup-ghc: false + + - name: Cache Pantry (Stackage package index) + id: pantry + uses: actions/cache@v6 + with: + path: ~/.stack/pantry + key: CI-pantry-${{ env.MANUAL_RESET }}-${{ matrix.resolver }} + + - name: Recompute Stackage package index + if: steps.pantry.outputs.cache-hit != 'true' + run: stack update # populates ~/.stack/pantry + + - name: Cache Dependencies + uses: actions/cache@v6 + with: + path: | + ~/.stack/stack.sqlite3 + ~/.stack/snapshots + key: ${{ runner.os }}-${{ matrix.resolver }}-stack + + - name: Build lts-12 + if: "${{ matrix.resolver == 'lts-12' }}" + run: | + stack config set system-ghc true --global + stack build --resolver=${{ matrix.resolver }} --ghc-options='-Wall' + + - name: Build + if: "${{ matrix.resolver != 'lts-12' }}" + run: | + stack config set system-ghc true --global + stack build --resolver=${{ matrix.resolver }} --test --bench \ + --no-run-tests --no-run-benchmarks --ghc-options='-Wall' + - name: Test + if: "${{ matrix.resolver != 'lts-12' }}" + run: | + stack test --no-rerun-tests http-types:test:spec --resolver=${{ matrix.resolver }} + - name: Test Docs + if: "${{ matrix.resolver != 'lts-12' }}" + run: | + stack test http-types:test:doctests --resolver=${{ matrix.resolver }} + # # We probably want to add benchmarks at some point, just to make sure # # functions don't regress in performance too much? # - name: Bench diff --git a/http-types.cabal b/http-types.cabal index b974c14..29bbc1b 100644 --- a/http-types.cabal +++ b/http-types.cabal @@ -1,4 +1,4 @@ -Cabal-version: 2.2 +Cabal-version: 1.22 Name: http-types Version: 0.13.0 Synopsis: Generic HTTP types for Haskell (for both client and server code). From b62398949de90f3ec3f4f6a3c8c0ba2834058741 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Wed, 19 Aug 2026 18:27:54 +0200 Subject: [PATCH 24/27] change version bump to 0.12.7 --- Network/HTTP/Types/Header.hs | 30 +++++++++++++++--------------- Network/HTTP/Types/Status.hs | 8 ++++---- Network/HTTP/Types/Version.hs | 10 +++++----- http-types.cabal | 2 +- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/Network/HTTP/Types/Header.hs b/Network/HTTP/Types/Header.hs index 72296d0..0688923 100644 --- a/Network/HTTP/Types/Header.hs +++ b/Network/HTTP/Types/Header.hs @@ -162,43 +162,43 @@ hAcceptEncoding = "Accept-Encoding" -- | [Access-Control-Allow-Credentials](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-origin-response-header) -- --- @since 0.13.0 +-- @since 0.12.7 hAccessControlAllowCredentials :: HeaderName hAccessControlAllowCredentials = "Access-Control-Allow-Credentials" -- | [Access-Control-Allow-Headers](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-headers-response-header) -- --- @since 0.13.0 +-- @since 0.12.7 hAccessControlAllowHeaders :: HeaderName hAccessControlAllowHeaders = "Access-Control-Allow-Headers" -- | [Access-Control-Allow-Methods](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-methods-response-header) -- --- @since 0.13.0 +-- @since 0.12.7 hAccessControlAllowMethods :: HeaderName hAccessControlAllowMethods = "Access-Control-Allow-Methods" -- | [Access-Control-Allow-Origin](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-allow-origin-response-header) -- --- @since 0.13.0 +-- @since 0.12.7 hAccessControlAllowOrigin :: HeaderName hAccessControlAllowOrigin = "Access-Control-Allow-Origin" -- | [Access-Control-Expose-Headers](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-expose-headers-response-header) -- --- @since 0.13.0 +-- @since 0.12.7 hAccessControlExposeHeaders :: HeaderName hAccessControlExposeHeaders = "Access-Control-Expose-Headers" -- | [Access-Control-Max-Age](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-max-age-response-header) -- --- @since 0.13.0 +-- @since 0.12.7 hAccessControlMaxAge :: HeaderName hAccessControlMaxAge = "Access-Control-Max-Age" -- | [Access-Control-Request-Method](https://www.w3.org/TR/2014/REC-cors-20140116/#access-control-request-method-request-header) -- --- @since 0.13.0 +-- @since 0.12.7 hAccessControlRequestMethod :: HeaderName hAccessControlRequestMethod = "Access-Control-Request-Method" @@ -480,13 +480,13 @@ hWarning = "Warning" -- | [Accept-Patch](https://www.rfc-editor.org/rfc/rfc5789.html#section-3.1) -- --- @since 0.13.0 +-- @since 0.12.7 hAcceptPatch :: HeaderName hAcceptPatch = "Accept-Patch" -- | [Alt-Svc](https://www.rfc-editor.org/rfc/rfc7838.html#section-3) -- --- @since 0.13.0 +-- @since 0.12.7 hAltSvc :: HeaderName hAltSvc = "Alt-Svc" @@ -498,19 +498,19 @@ hContentDisposition = "Content-Disposition" -- | [Content-Digest](https://www.rfc-editor.org/rfc/rfc9530.html#section-2) -- --- @since 0.13.0 +-- @since 0.12.7 hContentDigest :: HeaderName hContentDigest = "Content-Digest" -- | [Content-Security-Policy](https://www.w3.org/TR/CSP3/#csp-header) -- --- @since 0.13.0 +-- @since 0.12.7 hContentSecurityPolicy :: HeaderName hContentSecurityPolicy = "Content-Security-Policy" -- | [Content-Security-Policy-Report-Only](https://www.w3.org/TR/CSP3/#cspro-header) -- --- @since 0.13.0 +-- @since 0.12.7 hContentSecurityPolicyReportOnly :: HeaderName hContentSecurityPolicyReportOnly = "Content-Security-Policy-Report-Only" @@ -522,13 +522,13 @@ hCookie = "Cookie" -- | [Forwarded](https://www.rfc-editor.org/rfc/rfc7239.html) -- --- @since 0.13.0 +-- @since 0.12.7 hForwarded :: HeaderName hForwarded = "Forwarded" -- | [Link](https://www.rfc-editor.org/rfc/rfc8288.html#section-3) -- --- @since 0.13.0 +-- @since 0.12.7 hLink :: HeaderName hLink = "Link" @@ -564,7 +564,7 @@ hSetCookie = "Set-Cookie" -- | [Strict-Transport-Security](https://www.rfc-editor.org/rfc/rfc6797.html#section-6.1) -- --- @since 0.13.0 +-- @since 0.12.7 hStrictTransportSecurity :: HeaderName hStrictTransportSecurity = "Strict-Transport-Security" diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 4365a5c..1f9d25b 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -1156,7 +1156,7 @@ statusIsServerError (Status{statusCode = code}) = code >= 500 && code < 600 -- /N.B. This function assumes @statusCode < 1000@!/ -- /If it is @>= 1000@, the first byte will not be a digit./ -- --- @since 0.13.0 +-- @since 0.12.7 renderStatusCodeToPtr :: Status -> Ptr Word8 -> IO () renderStatusCodeToPtr (Status code _) ptr = do poke ptr $ toByte h @@ -1170,7 +1170,7 @@ renderStatusCodeToPtr (Status code _) ptr = do -- | Render the 3 digit t'Status' code into a 'ByteString'. -- --- @since 0.13.0 +-- @since 0.12.7 renderStatusCode :: Status -> ByteString renderStatusCode s@(Status code _) | code >= 1000 = B8.pack $ show s @@ -1181,7 +1181,7 @@ renderStatusCode s@(Status code _) -- -- /N.B. Same caveat from 'renderStatusCodeToPtr' applies./ -- --- @since 0.13.0 +-- @since 0.12.7 renderFullStatusToPtr :: Status -> Ptr Word8 -> IO () renderFullStatusToPtr s@(Status _ (PS fptr offset len)) ptr = do renderStatusCodeToPtr s ptr @@ -1191,7 +1191,7 @@ renderFullStatusToPtr s@(Status _ (PS fptr offset len)) ptr = do -- | Render the full t'Status' code with status message into a 'ByteString'. -- --- @since 0.13.0 +-- @since 0.12.7 renderFullStatus :: Status -> ByteString renderFullStatus s@(Status code msg) | code >= 1000 = diff --git a/Network/HTTP/Types/Version.hs b/Network/HTTP/Types/Version.hs index 66ddba6..e79943f 100644 --- a/Network/HTTP/Types/Version.hs +++ b/Network/HTTP/Types/Version.hs @@ -101,17 +101,17 @@ pattern Http11 :: HttpVersion pattern Http20 :: HttpVersion pattern Http30 :: HttpVersion --- | @since 0.13.0 +-- | @since 0.12.7 pattern Http09 = HttpVersion 0 9 --- | @since 0.13.0 +-- | @since 0.12.7 pattern Http10 = HttpVersion 1 0 --- | @since 0.13.0 +-- | @since 0.12.7 pattern Http11 = HttpVersion 1 1 --- | @since 0.13.0 +-- | @since 0.12.7 pattern Http20 = HttpVersion 2 0 --- | @since 0.13.0 +-- | @since 0.12.7 pattern Http30 = HttpVersion 3 0 diff --git a/http-types.cabal b/http-types.cabal index 29bbc1b..f9b9782 100644 --- a/http-types.cabal +++ b/http-types.cabal @@ -1,6 +1,6 @@ Cabal-version: 1.22 Name: http-types -Version: 0.13.0 +Version: 0.12.7 Synopsis: Generic HTTP types for Haskell (for both client and server code). Description: Types and functions to describe and handle HTTP concepts. Including "methods", "headers", "query strings", "paths" and "HTTP versions". From a5e7c7b3d54934de2ba82435b8e023728e2c6057 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Wed, 19 Aug 2026 18:29:42 +0200 Subject: [PATCH 25/27] change 'Status' pattern synonym to 'StatusCode' instead of individual ones. and small tweak to render functions --- CHANGELOG.md | 3 +- Network/HTTP/Types/Status.hs | 324 +++-------------------------------- 2 files changed, 28 insertions(+), 299 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1b8028..5742643 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,7 @@ * `Http11` * `Http20` * `Http30` -* Add pattern synonyms for `Status` - * all of format `StatusXXX` +* Add pattern synonyms for `Status` as `StatusCode` to only match the 3 digit code * Add more constant headers: * `hAcceptPatch` * `hAccessControlAllowCredentials` diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index 1f9d25b..ae595d7 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -10,107 +10,10 @@ module Network.HTTP.Types.Status ( -- * HTTP Status #if __GLASGOW_HASKELL__ >= 800 - Status ( - Status, - Status100, - Status101, - Status200, - Status201, - Status202, - Status203, - Status204, - Status205, - Status206, - Status300, - Status301, - Status302, - Status303, - Status304, - Status305, - Status307, - Status308, - Status400, - Status401, - Status402, - Status403, - Status404, - Status405, - Status407, - Status408, - Status409, - Status410, - Status411, - Status412, - Status413, - Status414, - Status415, - Status416, - Status417, - Status418, - Status422, - Status426, - Status428, - Status429, - Status431, - Status451, - Status500, - Status501, - Status502, - Status503, - Status504, - Status505, - Status511 - ), + Status (Status, StatusCode), #else Status(Status), - pattern Status100, - pattern Status101, - pattern Status200, - pattern Status201, - pattern Status202, - pattern Status203, - pattern Status204, - pattern Status205, - pattern Status206, - pattern Status300, - pattern Status301, - pattern Status302, - pattern Status303, - pattern Status304, - pattern Status305, - pattern Status307, - pattern Status308, - pattern Status400, - pattern Status401, - pattern Status402, - pattern Status403, - pattern Status404, - pattern Status405, - pattern Status407, - pattern Status408, - pattern Status409, - pattern Status410, - pattern Status411, - pattern Status412, - pattern Status413, - pattern Status414, - pattern Status415, - pattern Status416, - pattern Status417, - pattern Status418, - pattern Status422, - pattern Status426, - pattern Status428, - pattern Status429, - pattern Status431, - pattern Status451, - pattern Status500, - pattern Status501, - pattern Status502, - pattern Status503, - pattern Status504, - pattern Status505, - pattern Status511, + pattern StatusCode, #endif statusCode, statusMessage, @@ -236,7 +139,7 @@ module Network.HTTP.Types.Status ( statusIsServerError, ) where -import Control.Monad (guard) +import Control.Monad (guard, when) import Data.Bits ((.&.), (.|.)) import Data.ByteString as B (ByteString, drop, empty, length, uncons) import qualified Data.ByteString.Char8 as B8 @@ -281,6 +184,23 @@ data Status = Status Generic ) +-- | If you only need to check whether the `statusCode` is a specific number, +-- this pattern can make matching on it a bit easier: +-- +-- @ +-- handleResponse :: Status -> IO () +-- handleResponse (StatusCode st) = +-- case st of +-- 200 -> handleOK +-- 401 -> reportAuthFailure +-- 500 -> retry +-- xxx -> abort xxx +-- @ +-- +-- @since 0.12.7 +pattern StatusCode :: Int -> Status +pattern StatusCode code <- Status code _ + -- | A t'Status' is equal to another t'Status' if the status codes are equal. instance Eq Status where (==) = (==) `on` statusCode @@ -385,14 +305,6 @@ status101 = mkStatus 101 "Switching Protocols" switchingProtocols101 :: Status switchingProtocols101 = status101 --- | @since 0.13.0 -pattern Status100 :: Status -pattern Status100 <- Status 100 _ - --- | @since 0.13.0 -pattern Status101 :: Status -pattern Status101 <- Status 101 _ - -- | OK 200 status200 :: Status status200 = mkStatus 200 "OK" @@ -469,34 +381,6 @@ status206 = mkStatus 206 "Partial Content" partialContent206 :: Status partialContent206 = status206 --- | @since 0.13.0 -pattern Status200 :: Status -pattern Status200 <- Status 200 _ - --- | @since 0.13.0 -pattern Status201 :: Status -pattern Status201 <- Status 201 _ - --- | @since 0.13.0 -pattern Status202 :: Status -pattern Status202 <- Status 202 _ - --- | @since 0.13.0 -pattern Status203 :: Status -pattern Status203 <- Status 203 _ - --- | @since 0.13.0 -pattern Status204 :: Status -pattern Status204 <- Status 204 _ - --- | @since 0.13.0 -pattern Status205 :: Status -pattern Status205 <- Status 205 _ - --- | @since 0.13.0 -pattern Status206 :: Status -pattern Status206 <- Status 206 _ - -- | Multiple Choices 300 status300 :: Status status300 = mkStatus 300 "Multiple Choices" @@ -577,39 +461,6 @@ status308 = mkStatus 308 "Permanent Redirect" permanentRedirect308 :: Status permanentRedirect308 = status308 - --- | @since 0.13.0 -pattern Status300 :: Status -pattern Status300 <- Status 300 _ - --- | @since 0.13.0 -pattern Status301 :: Status -pattern Status301 <- Status 301 _ - --- | @since 0.13.0 -pattern Status302 :: Status -pattern Status302 <- Status 302 _ - --- | @since 0.13.0 -pattern Status303 :: Status -pattern Status303 <- Status 303 _ - --- | @since 0.13.0 -pattern Status304 :: Status -pattern Status304 <- Status 304 _ - --- | @since 0.13.0 -pattern Status305 :: Status -pattern Status305 <- Status 305 _ - --- | @since 0.13.0 -pattern Status307 :: Status -pattern Status307 <- Status 307 _ - --- | @since 0.13.0 -pattern Status308 :: Status -pattern Status308 <- Status 308 _ - -- | Bad Request 400 status400 :: Status status400 = mkStatus 400 "Bad Request" @@ -905,102 +756,6 @@ status451 = mkStatus 451 "Unavailable For Legal Reasons" unavailableForLegalReasons451 :: Status unavailableForLegalReasons451 = status451 --- | @since 0.13.0 -pattern Status400 :: Status -pattern Status400 <- Status 400 _ - --- | @since 0.13.0 -pattern Status401 :: Status -pattern Status401 <- Status 401 _ - --- | @since 0.13.0 -pattern Status402 :: Status -pattern Status402 <- Status 402 _ - --- | @since 0.13.0 -pattern Status403 :: Status -pattern Status403 <- Status 403 _ - --- | @since 0.13.0 -pattern Status404 :: Status -pattern Status404 <- Status 404 _ - --- | @since 0.13.0 -pattern Status405 :: Status -pattern Status405 <- Status 405 _ - --- | @since 0.13.0 -pattern Status407 :: Status -pattern Status407 <- Status 407 _ - --- | @since 0.13.0 -pattern Status408 :: Status -pattern Status408 <- Status 408 _ - --- | @since 0.13.0 -pattern Status409 :: Status -pattern Status409 <- Status 409 _ - --- | @since 0.13.0 -pattern Status410 :: Status -pattern Status410 <- Status 410 _ - --- | @since 0.13.0 -pattern Status411 :: Status -pattern Status411 <- Status 411 _ - --- | @since 0.13.0 -pattern Status412 :: Status -pattern Status412 <- Status 412 _ - --- | @since 0.13.0 -pattern Status413 :: Status -pattern Status413 <- Status 413 _ - --- | @since 0.13.0 -pattern Status414 :: Status -pattern Status414 <- Status 414 _ - --- | @since 0.13.0 -pattern Status415 :: Status -pattern Status415 <- Status 415 _ - --- | @since 0.13.0 -pattern Status416 :: Status -pattern Status416 <- Status 416 _ - --- | @since 0.13.0 -pattern Status417 :: Status -pattern Status417 <- Status 417 _ - --- | @since 0.13.0 -pattern Status418 :: Status -pattern Status418 <- Status 418 _ - --- | @since 0.13.0 -pattern Status422 :: Status -pattern Status422 <- Status 422 _ - --- | @since 0.13.0 -pattern Status426 :: Status -pattern Status426 <- Status 426 _ - --- | @since 0.13.0 -pattern Status428 :: Status -pattern Status428 <- Status 428 _ - --- | @since 0.13.0 -pattern Status429 :: Status -pattern Status429 <- Status 429 _ - --- | @since 0.13.0 -pattern Status431 :: Status -pattern Status431 <- Status 431 _ - --- | @since 0.13.0 -pattern Status451 :: Status -pattern Status451 <- Status 451 _ - -- | Internal Server Error 500 status500 :: Status status500 = mkStatus 500 "Internal Server Error" @@ -1083,34 +838,6 @@ status511 = mkStatus 511 "Network Authentication Required" networkAuthenticationRequired511 :: Status networkAuthenticationRequired511 = status511 --- | @since 0.13.0 -pattern Status500 :: Status -pattern Status500 <- Status 500 _ - --- | @since 0.13.0 -pattern Status501 :: Status -pattern Status501 <- Status 501 _ - --- | @since 0.13.0 -pattern Status502 :: Status -pattern Status502 <- Status 502 _ - --- | @since 0.13.0 -pattern Status503 :: Status -pattern Status503 <- Status 503 _ - --- | @since 0.13.0 -pattern Status504 :: Status -pattern Status504 <- Status 504 _ - --- | @since 0.13.0 -pattern Status505 :: Status -pattern Status505 <- Status 505 _ - --- | @since 0.13.0 -pattern Status511 :: Status -pattern Status511 <- Status 511 _ - -- | Informational class -- -- Checks if the status is in the 1XX range. @@ -1167,6 +894,7 @@ renderStatusCodeToPtr (Status code _) ptr = do (t, i) = rest `divMod` 10 toByte :: Int -> Word8 toByte x = fromIntegral x .|. 0x30 +{-# INLINABLE renderStatusCodeToPtr #-} -- | Render the 3 digit t'Status' code into a 'ByteString'. -- @@ -1177,7 +905,7 @@ renderStatusCode s@(Status code _) | otherwise = unsafeCreate 3 $ renderStatusCodeToPtr s --- | Writes the full t'Status' code to the provided 'Ptr'. +-- | Writes the full t'Status' code and message to the provided 'Ptr'. -- -- /N.B. Same caveat from 'renderStatusCodeToPtr' applies./ -- @@ -1185,9 +913,11 @@ renderStatusCode s@(Status code _) renderFullStatusToPtr :: Status -> Ptr Word8 -> IO () renderFullStatusToPtr s@(Status _ (PS fptr offset len)) ptr = do renderStatusCodeToPtr s ptr - poke (ptr `plusPtr` 3) (0x20 :: Word8) - unsafeWithForeignPtr fptr $ \src -> - copyBytes (ptr `plusPtr` 4) (src `plusPtr` offset) len + poke (ptr `plusPtr` 3) (0x20 :: Word8) -- space + when (len > 0) $ + unsafeWithForeignPtr fptr $ \src -> + copyBytes (ptr `plusPtr` 4) (src `plusPtr` offset) len +{-# INLINABLE renderFullStatusToPtr #-} -- | Render the full t'Status' code with status message into a 'ByteString'. -- From 88ae28f5998f94d23272d1ab7928c665636968ea Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Wed, 19 Aug 2026 18:46:36 +0200 Subject: [PATCH 26/27] test: fix 'Status' pattern synonym tests and add 'COMPLETE' pragma to 'StatusCode' --- Network/HTTP/Types/Status.hs | 1 + test/Network/HTTP/Types/StatusSpec.hs | 60 +++------------------------ 2 files changed, 6 insertions(+), 55 deletions(-) diff --git a/Network/HTTP/Types/Status.hs b/Network/HTTP/Types/Status.hs index ae595d7..aec5122 100644 --- a/Network/HTTP/Types/Status.hs +++ b/Network/HTTP/Types/Status.hs @@ -200,6 +200,7 @@ data Status = Status -- @since 0.12.7 pattern StatusCode :: Int -> Status pattern StatusCode code <- Status code _ +{-# COMPLETE StatusCode #-} -- | A t'Status' is equal to another t'Status' if the status codes are equal. instance Eq Status where diff --git a/test/Network/HTTP/Types/StatusSpec.hs b/test/Network/HTTP/Types/StatusSpec.hs index 86fb681..69370a6 100644 --- a/test/Network/HTTP/Types/StatusSpec.hs +++ b/test/Network/HTTP/Types/StatusSpec.hs @@ -9,7 +9,7 @@ import qualified Data.ByteString.Char8 as B8 import Data.Function (on) import qualified Data.List as L import Test.Hspec -import Test.QuickCheck (Arbitrary (..), choose, property, resize) +import Test.QuickCheck (Arbitrary (..), choose, property, resize, (===)) import Test.QuickCheck.Instances () import Network.HTTP.Types @@ -67,60 +67,10 @@ spec = do -- it's about the status code here. parseStatusCode (renderStatusCode notFound404) `shouldBe` Just (404, "") describe "Patterns" $ do - patternMatch status100 (\Status100 -> pure ()) - patternMatch status101 (\Status101 -> pure ()) - patternMatch status200 (\Status200 -> pure ()) - patternMatch status201 (\Status201 -> pure ()) - patternMatch status202 (\Status202 -> pure ()) - patternMatch status203 (\Status203 -> pure ()) - patternMatch status204 (\Status204 -> pure ()) - patternMatch status205 (\Status205 -> pure ()) - patternMatch status206 (\Status206 -> pure ()) - patternMatch status300 (\Status300 -> pure ()) - patternMatch status301 (\Status301 -> pure ()) - patternMatch status302 (\Status302 -> pure ()) - patternMatch status303 (\Status303 -> pure ()) - patternMatch status304 (\Status304 -> pure ()) - patternMatch status305 (\Status305 -> pure ()) - patternMatch status307 (\Status307 -> pure ()) - patternMatch status308 (\Status308 -> pure ()) - patternMatch status400 (\Status400 -> pure ()) - patternMatch status401 (\Status401 -> pure ()) - patternMatch status402 (\Status402 -> pure ()) - patternMatch status403 (\Status403 -> pure ()) - patternMatch status404 (\Status404 -> pure ()) - patternMatch status405 (\Status405 -> pure ()) - patternMatch status407 (\Status407 -> pure ()) - patternMatch status408 (\Status408 -> pure ()) - patternMatch status409 (\Status409 -> pure ()) - patternMatch status410 (\Status410 -> pure ()) - patternMatch status411 (\Status411 -> pure ()) - patternMatch status412 (\Status412 -> pure ()) - patternMatch status413 (\Status413 -> pure ()) - patternMatch status414 (\Status414 -> pure ()) - patternMatch status415 (\Status415 -> pure ()) - patternMatch status416 (\Status416 -> pure ()) - patternMatch status417 (\Status417 -> pure ()) - patternMatch status418 (\Status418 -> pure ()) - patternMatch status422 (\Status422 -> pure ()) - patternMatch status426 (\Status426 -> pure ()) - patternMatch status428 (\Status428 -> pure ()) - patternMatch status429 (\Status429 -> pure ()) - patternMatch status431 (\Status431 -> pure ()) - patternMatch status451 (\Status451 -> pure ()) - patternMatch status500 (\Status500 -> pure ()) - patternMatch status501 (\Status501 -> pure ()) - patternMatch status502 (\Status502 -> pure ()) - patternMatch status503 (\Status503 -> pure ()) - patternMatch status504 (\Status504 -> pure ()) - patternMatch status505 (\Status505 -> pure ()) - patternMatch status511 (\Status511 -> pure ()) - -patternMatch :: Status -> (Status -> IO ()) -> SpecWith () -patternMatch s f = - it name $ f s `shouldReturn` () - where - name = "match the status " <> show (statusCode s) + it "matches on StatusCode" $ + property $ \st -> + case st of + StatusCode code -> code === statusCode st categoryCheck :: String -> (Status -> Bool) -> [StatusTuple] -> Spec categoryCheck name p shoulds = do From 5dc3291b4b0a5ff6a00959335ca1b017529ceb30 Mon Sep 17 00:00:00 2001 From: Felix Paulusma Date: Wed, 19 Aug 2026 18:51:27 +0200 Subject: [PATCH 27/27] ci: cache cabal --- .github/workflows/ci.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0374ad8..3be0a83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ on: jobs: cabal: env: - MANUAL_RESET: v4 + MANUAL_RESET: v5 name: cabal / ghc-${{matrix.ghc}} / ${{ matrix.os }} continue-on-error: ${{ matrix.ghc == '9.14.1'}} runs-on: ${{ matrix.os }} @@ -47,6 +47,7 @@ jobs: ~/.ghcup/cache/* ~/.ghcup/config.yaml ~/.ghcup/ghc/${{ matrix.ghc }} + ~/.ghcup/cabal/**/cabal key: CI-ghcup-cabal-${{ env.MANUAL_RESET }}-${{ matrix.ghc }} - name: Setup Haskell