File
Blob: src/workerd/io/compatibility-date-test.c++
| 1 | // Copyright (c) 2017-2022 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | |
| 5 | #include "compatibility-date.h" |
| 6 | |
| 7 | #include <workerd/io/maximum-compatibility-date.embed.h> |
| 8 | |
| 9 | #include <capnp/message.h> |
| 10 | #include <capnp/serialize-text.h> |
| 11 | #include <kj/debug.h> |
| 12 | #include <kj/test.h> |
| 13 | |
| 14 | #include <chrono> |
| 15 | #include <format> |
| 16 | |
| 17 | namespace workerd { |
| 18 | namespace { |
| 19 | |
| 20 | KJ_TEST("compatibility date parsing") { |
| 21 | auto expectParseTo = [](kj::StringPtr input, kj::StringPtr expected) { |
| 22 | KJ_IF_SOME(actual, normalizeCompatDate(input)) { |
| 23 | KJ_EXPECT(actual == expected); |
| 24 | } else { |
| 25 | KJ_FAIL_EXPECT("couldn't parse", input); |
| 26 | } |
| 27 | }; |
| 28 | |
| 29 | auto expectNoParse = [](kj::StringPtr input) { |
| 30 | KJ_IF_SOME(actual, normalizeCompatDate(input)) { |
| 31 | KJ_FAIL_EXPECT("expected couldn't parse", input, actual); |
| 32 | } |
| 33 | }; |
| 34 | |
| 35 | expectParseTo("2021-05-17", "2021-05-17"); |
| 36 | expectParseTo("2021-05-01", "2021-05-01"); |
| 37 | expectParseTo("2000-01-01", "2000-01-01"); |
| 38 | expectParseTo("2999-12-31", "2999-12-31"); |
| 39 | expectParseTo("2024-02-29", "2024-02-29"); |
| 40 | expectParseTo("2112-04-01", "2112-04-01"); |
| 41 | |
| 42 | // Alas, strptime() accepts February 30 as a perfectly valid date. |
| 43 | //expectNoParse("2024-2-30"); |
| 44 | //expectNoParse("2023-2-29"); |
| 45 | |
| 46 | expectNoParse("2024-2-32"); |
| 47 | expectNoParse("3000-01-01"); |
| 48 | expectNoParse("1999-12-31"); |
| 49 | expectNoParse("123-01-01"); |
| 50 | expectNoParse("2021-13-01"); |
| 51 | expectNoParse("2021-12-32"); |
| 52 | expectNoParse("2021-00-01"); |
| 53 | expectNoParse("2021-01-00"); |
| 54 | |
| 55 | expectNoParse(" 2021-05-17"); |
| 56 | expectNoParse("2021 -05-17"); |
| 57 | expectNoParse("2021- 05-17"); |
| 58 | expectNoParse("2021-05 -17"); |
| 59 | expectNoParse("2021-05- 17"); |
| 60 | expectNoParse("2021-05-17 "); |
| 61 | expectNoParse("2021/05/17"); |
| 62 | expectNoParse("2021_05_17"); |
| 63 | |
| 64 | expectNoParse("2021-5-07"); |
| 65 | expectNoParse("2021-05-7"); |
| 66 | expectNoParse("202-05-07"); |
| 67 | } |
| 68 | |
| 69 | KJ_TEST("compatibility flag parsing") { |
| 70 | auto expectCompileCompatibilityFlags = |
| 71 | [](kj::StringPtr compatDate, kj::ArrayPtr<const kj::StringPtr> featureFlags, |
| 72 | kj::StringPtr expectedOutput, kj::ArrayPtr<const kj::StringPtr> expectedErrors = nullptr, |
| 73 | CompatibilityDateValidation dateValidation = CompatibilityDateValidation::FUTURE_FOR_TEST, |
| 74 | bool r2InternalBetaApiSet = false, bool experimental = false) { |
| 75 | capnp::MallocMessageBuilder message; |
| 76 | auto orphanage = message.getOrphanage(); |
| 77 | |
| 78 | auto flagListOrphan = orphanage.newOrphan<capnp::List<capnp::Text>>(featureFlags.size()); |
| 79 | auto flagList = flagListOrphan.get(); |
| 80 | for (auto i: kj::indices(featureFlags)) { |
| 81 | flagList.set(i, featureFlags.begin()[i]); |
| 82 | } |
| 83 | |
| 84 | auto outputOrphan = orphanage.newOrphan<CompatibilityFlags>(); |
| 85 | auto output = outputOrphan.get(); |
| 86 | |
| 87 | SimpleWorkerErrorReporter errorReporter; |
| 88 | compileCompatibilityFlags( |
| 89 | compatDate, flagList.asReader(), output, errorReporter, experimental, dateValidation); |
| 90 | |
| 91 | capnp::TextCodec codec; |
| 92 | auto parsedExpectedOutput = codec.decode<CompatibilityFlags>(expectedOutput, orphanage); |
| 93 | |
| 94 | if (!r2InternalBetaApiSet) { |
| 95 | // The r2PublicBetaApi is always expected by default regardless of compat date unless |
| 96 | // explicitly disabled. |
| 97 | parsedExpectedOutput.get().setR2PublicBetaApi(true); |
| 98 | } |
| 99 | |
| 100 | // If errors are expected, then the output is irrelevant. |
| 101 | if (expectedErrors.size() == 0) { |
| 102 | KJ_EXPECT(kj::str(output) == kj::str(parsedExpectedOutput.getReader())); |
| 103 | } |
| 104 | KJ_EXPECT(kj::strArray(errorReporter.errors, "\n") == kj::strArray(expectedErrors, "\n")); |
| 105 | }; |
| 106 | |
| 107 | expectCompileCompatibilityFlags("2021-05-17", {}, "()"); |
| 108 | |
| 109 | expectCompileCompatibilityFlags("2021-11-02", {}, "(formDataParserSupportsFiles = false)"); |
| 110 | expectCompileCompatibilityFlags("2021-11-03", {}, "(formDataParserSupportsFiles = true)"); |
| 111 | expectCompileCompatibilityFlags("2021-11-04", {}, "(formDataParserSupportsFiles = true)"); |
| 112 | expectCompileCompatibilityFlags("2021-11-03", {"formdata_parser_converts_files_to_strings"}, |
| 113 | "(formDataParserSupportsFiles = false)"); |
| 114 | |
| 115 | // Test compatibility flag overrides. |
| 116 | expectCompileCompatibilityFlags( |
| 117 | "2021-05-17", {"formdata_parser_supports_files"_kj}, "(formDataParserSupportsFiles = true)"); |
| 118 | expectCompileCompatibilityFlags("2021-05-17", {"fetch_refuses_unknown_protocols"_kj}, |
| 119 | "(fetchRefusesUnknownProtocols = true)"); |
| 120 | expectCompileCompatibilityFlags("2021-05-17", |
| 121 | {"formdata_parser_supports_files"_kj, "fetch_refuses_unknown_protocols"_kj}, |
| 122 | "(formDataParserSupportsFiles = true, fetchRefusesUnknownProtocols = true)"); |
| 123 | expectCompileCompatibilityFlags("2021-11-04", {"fetch_refuses_unknown_protocols"_kj}, |
| 124 | "(formDataParserSupportsFiles = true, fetchRefusesUnknownProtocols = true)"); |
| 125 | |
| 126 | // Test errors. |
| 127 | expectCompileCompatibilityFlags("abcd", {}, "()", {"Invalid compatibility date: abcd"}); |
| 128 | expectCompileCompatibilityFlags("2021-05-17", |
| 129 | {"formdata_parser_supports_files"_kj, "formdata_parser_supports_files"_kj}, |
| 130 | "(formDataParserSupportsFiles = true)", |
| 131 | {"Compatibility flag specified multiple times: formdata_parser_supports_files"}); |
| 132 | expectCompileCompatibilityFlags("2021-05-17", |
| 133 | {"formdata_parser_supports_files"_kj, "formdata_parser_converts_files_to_strings"_kj}, |
| 134 | "(formDataParserSupportsFiles = true)", |
| 135 | {"Compatibility flags are mutually contradictory: " |
| 136 | "formdata_parser_supports_files vs formdata_parser_converts_files_to_strings"}); |
| 137 | expectCompileCompatibilityFlags("2021-11-04", {"formdata_parser_supports_files"_kj}, |
| 138 | "(formDataParserSupportsFiles = true)", |
| 139 | {"The compatibility flag formdata_parser_supports_files became the default as of " |
| 140 | "2021-11-03 so does not need to be specified anymore."}, |
| 141 | CompatibilityDateValidation::CURRENT_DATE_FOR_CLOUDFLARE); |
| 142 | expectCompileCompatibilityFlags( |
| 143 | "2021-05-17", {"unknown_feature"_kj}, "()", {"No such compatibility flag: unknown_feature"}); |
| 144 | |
| 145 | expectCompileCompatibilityFlags("2252-04-01", {}, "()", |
| 146 | {"Can't set compatibility date in the future: 2252-04-01"}, |
| 147 | CompatibilityDateValidation::CURRENT_DATE_FOR_CLOUDFLARE); |
| 148 | |
| 149 | expectCompileCompatibilityFlags("2252-04-01", {}, "()", |
| 150 | {kj::str("This Worker requires compatibility date \"2252-04-01\", but the newest date " |
| 151 | "supported by this server binary is \"", |
| 152 | MAXIMUM_COMPATIBILITY_DATE, "\"."), |
| 153 | kj::str("Can't set compatibility date in the future: \"2252-04-01\". Today's date " |
| 154 | "(UTC) is \"", |
| 155 | currentDateStr(), "\".")}, |
| 156 | CompatibilityDateValidation::CODE_VERSION); |
| 157 | |
| 158 | // Test experimental requirement using durable_object_rename as it is obsolete |
| 159 | expectCompileCompatibilityFlags("2020-01-01", {"durable_object_rename"_kj}, "(obsolete19 = true)", |
| 160 | {"The compatibility flag durable_object_rename is experimental and may break or be removed " |
| 161 | "in a future version of workerd. To use this flag, you must pass --experimental on the " |
| 162 | "command line."_kj}, |
| 163 | CompatibilityDateValidation::CODE_VERSION, false, false); |
| 164 | expectCompileCompatibilityFlags("2020-01-01", {"durable_object_rename"_kj}, "(obsolete19 = true)", |
| 165 | {}, CompatibilityDateValidation::CODE_VERSION, false, true); |
| 166 | |
| 167 | // Test experimental requirement using the durable_object_alarms flag since we know this flag |
| 168 | // is obsolete and will never have a date set. (Should always pass, even if experimental flags |
| 169 | // aren't allowed) |
| 170 | expectCompileCompatibilityFlags("2020-01-01", {"durable_object_alarms"_kj}, "(obsolete14 = true)", |
| 171 | {}, CompatibilityDateValidation::CODE_VERSION, false, false); |
| 172 | expectCompileCompatibilityFlags("2020-01-01", {"durable_object_alarms"_kj}, "(obsolete14 = true)", |
| 173 | {}, CompatibilityDateValidation::CODE_VERSION, false, true); |
| 174 | |
| 175 | // Multiple errors. |
| 176 | expectCompileCompatibilityFlags("abcd", |
| 177 | {"formdata_parser_supports_files"_kj, "fetch_refuses_unknown_protocols"_kj, |
| 178 | "unknown_feature"_kj, "fetch_refuses_unknown_protocols"_kj, "another_feature"_kj, |
| 179 | "formdata_parser_supports_files"_kj}, |
| 180 | "(formDataParserSupportsFiles = true, fetchRefusesUnknownProtocols = true)", |
| 181 | {"Compatibility flag specified multiple times: fetch_refuses_unknown_protocols", |
| 182 | "Compatibility flag specified multiple times: formdata_parser_supports_files", |
| 183 | "Invalid compatibility date: abcd", "No such compatibility flag: another_feature", |
| 184 | "No such compatibility flag: unknown_feature"}); |
| 185 | |
| 186 | // Can explicitly disable flag that's enabled for all dates.s |
| 187 | expectCompileCompatibilityFlags("2021-05-17", {"r2_internal_beta_bindings"}, "()", {}, |
| 188 | CompatibilityDateValidation::FUTURE_FOR_TEST, true, false); |
| 189 | |
| 190 | // nodejs_compat implies nodejs_compat_v2 on or after 2024-09-23 |
| 191 | expectCompileCompatibilityFlags("2024-09-23", {"nodejs_compat"}, |
| 192 | "(formDataParserSupportsFiles = true," |
| 193 | " fetchRefusesUnknownProtocols = true," |
| 194 | " esiIncludeIsVoidTag = false," |
| 195 | " obsolete3 = false," |
| 196 | " durableObjectFetchRequiresSchemeAuthority = true," |
| 197 | " streamsByobReaderDetachesBuffer = true," |
| 198 | " streamsJavaScriptControllers = true," |
| 199 | " jsgPropertyOnPrototypeTemplate = true," |
| 200 | " minimalSubrequests = true," |
| 201 | " noCotsOnExternalFetch = true," |
| 202 | " specCompliantUrl = true," |
| 203 | " globalNavigator = true," |
| 204 | " captureThrowsAsRejections = true," |
| 205 | " r2PublicBetaApi = true," |
| 206 | " obsolete14 = false," |
| 207 | " noSubstituteNull = true," |
| 208 | " transformStreamJavaScriptControllers = true," |
| 209 | " r2ListHonorIncludeFields = true," |
| 210 | " exportCommonJsDefaultNamespace = true," |
| 211 | " obsolete19 = false," |
| 212 | " webSocketCompression = true," |
| 213 | " nodeJsCompat = true," |
| 214 | " obsolete22 = false," |
| 215 | " specCompliantResponseRedirect = true," |
| 216 | " workerdExperimental = false," |
| 217 | " durableObjectGetExisting = false," |
| 218 | " httpHeadersGetSetCookie = true," |
| 219 | " dispatchExceptionTunneling = true," |
| 220 | " serviceBindingExtraHandlers = false," |
| 221 | " noCfBotManagementDefault = true," |
| 222 | " urlSearchParamsDeleteHasValueArg = true," |
| 223 | " strictCompression = true," |
| 224 | " brotliContentEncoding = true," |
| 225 | " strictCrypto = true," |
| 226 | " rttiApi = false," |
| 227 | " obsolete35 = false," |
| 228 | " cryptoPreservePublicExponent = true," |
| 229 | " vectorizeQueryMetadataOptional = true," |
| 230 | " unsafeModule = false," |
| 231 | " jsRpc = false," |
| 232 | " noImportScripts = true," |
| 233 | " nodeJsAls = false," |
| 234 | " queuesJsonMessages = true," |
| 235 | " pythonWorkers = false," |
| 236 | " fetcherNoGetPutDelete = true," |
| 237 | " unwrapCustomThenables = true," |
| 238 | " fetcherRpc = true," |
| 239 | " internalStreamByobReturn = true," |
| 240 | " blobStandardMimeType = true," |
| 241 | " fetchStandardUrl = true," |
| 242 | " nodeJsCompatV2 = true," |
| 243 | " globalFetchStrictlyPublic = false," |
| 244 | " newModuleRegistry = false," |
| 245 | " allowCustomPorts = true," |
| 246 | " internalWritableStreamAbortClearsQueue = true," |
| 247 | " nodeJsZlib = true)", |
| 248 | {}, CompatibilityDateValidation::FUTURE_FOR_TEST, false, false); |
| 249 | expectCompileCompatibilityFlags("2024-09-22", {"nodejs_compat"}, |
| 250 | "(formDataParserSupportsFiles = true," |
| 251 | " fetchRefusesUnknownProtocols = true," |
| 252 | " esiIncludeIsVoidTag = false," |
| 253 | " obsolete3 = false," |
| 254 | " durableObjectFetchRequiresSchemeAuthority = true," |
| 255 | " streamsByobReaderDetachesBuffer = true," |
| 256 | " streamsJavaScriptControllers = true," |
| 257 | " jsgPropertyOnPrototypeTemplate = true," |
| 258 | " minimalSubrequests = true," |
| 259 | " noCotsOnExternalFetch = true," |
| 260 | " specCompliantUrl = true," |
| 261 | " globalNavigator = true," |
| 262 | " captureThrowsAsRejections = true," |
| 263 | " r2PublicBetaApi = true," |
| 264 | " obsolete14 = false," |
| 265 | " noSubstituteNull = true," |
| 266 | " transformStreamJavaScriptControllers = true," |
| 267 | " r2ListHonorIncludeFields = true," |
| 268 | " exportCommonJsDefaultNamespace = true," |
| 269 | " obsolete19 = false," |
| 270 | " webSocketCompression = true," |
| 271 | " nodeJsCompat = true," |
| 272 | " obsolete22 = false," |
| 273 | " specCompliantResponseRedirect = true," |
| 274 | " workerdExperimental = false," |
| 275 | " durableObjectGetExisting = false," |
| 276 | " httpHeadersGetSetCookie = true," |
| 277 | " dispatchExceptionTunneling = true," |
| 278 | " serviceBindingExtraHandlers = false," |
| 279 | " noCfBotManagementDefault = true," |
| 280 | " urlSearchParamsDeleteHasValueArg = true," |
| 281 | " strictCompression = true," |
| 282 | " brotliContentEncoding = true," |
| 283 | " strictCrypto = true," |
| 284 | " rttiApi = false," |
| 285 | " obsolete35 = false," |
| 286 | " cryptoPreservePublicExponent = true," |
| 287 | " vectorizeQueryMetadataOptional = true," |
| 288 | " unsafeModule = false," |
| 289 | " jsRpc = false," |
| 290 | " noImportScripts = true," |
| 291 | " nodeJsAls = false," |
| 292 | " queuesJsonMessages = true," |
| 293 | " pythonWorkers = false," |
| 294 | " fetcherNoGetPutDelete = true," |
| 295 | " unwrapCustomThenables = true," |
| 296 | " fetcherRpc = true," |
| 297 | " internalStreamByobReturn = true," |
| 298 | " blobStandardMimeType = true," |
| 299 | " fetchStandardUrl = true," |
| 300 | " nodeJsCompatV2 = false," |
| 301 | " globalFetchStrictlyPublic = false," |
| 302 | " newModuleRegistry = false," |
| 303 | " cacheOptionEnabled = false," |
| 304 | " kvDirectBinding = false," |
| 305 | " allowCustomPorts = true," |
| 306 | " increaseWebsocketMessageSize = false," |
| 307 | " internalWritableStreamAbortClearsQueue = true," |
| 308 | " pythonWorkersDevPyodide = false," |
| 309 | " nodeJsZlib = false)", |
| 310 | {}, CompatibilityDateValidation::FUTURE_FOR_TEST, false, false); |
| 311 | } |
| 312 | |
| 313 | KJ_TEST("encode to flag list for FL") { |
| 314 | capnp::MallocMessageBuilder message; |
| 315 | auto orphanage = message.getOrphanage(); |
| 316 | |
| 317 | auto compileOwnFeatureFlags = |
| 318 | [&](kj::StringPtr compatDate, kj::ArrayPtr<const kj::StringPtr> featureFlags, |
| 319 | CompatibilityDateValidation dateValidation = CompatibilityDateValidation::FUTURE_FOR_TEST, |
| 320 | bool experimental = false) { |
| 321 | auto flagListOrphan = orphanage.newOrphan<capnp::List<capnp::Text>>(featureFlags.size()); |
| 322 | auto flagList = flagListOrphan.get(); |
| 323 | for (auto i: kj::indices(featureFlags)) { |
| 324 | flagList.set(i, featureFlags.begin()[i]); |
| 325 | } |
| 326 | |
| 327 | auto outputOrphan = orphanage.newOrphan<CompatibilityFlags>(); |
| 328 | auto output = outputOrphan.get(); |
| 329 | |
| 330 | SimpleWorkerErrorReporter errorReporter; |
| 331 | |
| 332 | compileCompatibilityFlags( |
| 333 | compatDate, flagList.asReader(), output, errorReporter, experimental, dateValidation); |
| 334 | KJ_ASSERT(errorReporter.errors.empty()); |
| 335 | |
| 336 | return kj::mv(outputOrphan); |
| 337 | }; |
| 338 | |
| 339 | { |
| 340 | // Disabled by date. |
| 341 | auto featureFlagsOrphan = compileOwnFeatureFlags("2021-05-17", {}); |
| 342 | auto featureFlags = featureFlagsOrphan.get(); |
| 343 | auto strings = decompileCompatibilityFlagsForFl(featureFlags); |
| 344 | KJ_EXPECT(strings.size() == 0); |
| 345 | } |
| 346 | |
| 347 | { |
| 348 | // Disabled by date, enabled by flag. |
| 349 | auto featureFlagsOrphan = compileOwnFeatureFlags("2021-05-17", {"minimal_subrequests"_kj}); |
| 350 | auto featureFlags = featureFlagsOrphan.get(); |
| 351 | auto strings = decompileCompatibilityFlagsForFl(featureFlags); |
| 352 | KJ_EXPECT(strings.size() == 1); |
| 353 | KJ_EXPECT(strings[0] == "minimal_subrequests"_kj); |
| 354 | } |
| 355 | |
| 356 | { |
| 357 | // Enabled by date. |
| 358 | auto featureFlagsOrphan = compileOwnFeatureFlags("2022-07-01", {}); |
| 359 | auto featureFlags = featureFlagsOrphan.get(); |
| 360 | auto strings = decompileCompatibilityFlagsForFl(featureFlags); |
| 361 | KJ_EXPECT(strings.size() == 2); |
| 362 | KJ_EXPECT(strings[0] == "minimal_subrequests"_kj); |
| 363 | KJ_EXPECT(strings[1] == "no_cots_on_external_fetch"_kj); |
| 364 | } |
| 365 | |
| 366 | { |
| 367 | // Enabled by date, disabled by flag. |
| 368 | auto featureFlagsOrphan = compileOwnFeatureFlags("2022-07-01", {"cots_on_external_fetch"}); |
| 369 | auto featureFlags = featureFlagsOrphan.get(); |
| 370 | auto strings = decompileCompatibilityFlagsForFl(featureFlags); |
| 371 | KJ_EXPECT(strings.size() == 1); |
| 372 | KJ_EXPECT(strings[0] == "minimal_subrequests"_kj); |
| 373 | } |
| 374 | } |
| 375 | |
| 376 | KJ_TEST("compatibility dates must be Tuesday, Wednesday, or Thursday") { |
| 377 | // List of specific flags that are allowed to use non-conformant dates |
| 378 | // (already deployed and can't be changed for compatibility reasons) |
| 379 | kj::HashSet<kj::StringPtr> allowedFlagExceptions; |
| 380 | allowedFlagExceptions.insertAll(std::initializer_list<kj::StringPtr>{ |
| 381 | // Existing non-conformant dates that are already deployed |
| 382 | "jsgPropertyOnPrototypeTemplate"_kj, // 2022-01-31 (Monday) |
| 383 | "specCompliantUrl"_kj, // 2022-10-31 (Monday) |
| 384 | "globalNavigator"_kj, // 2022-03-21 (Monday) |
| 385 | "captureThrowsAsRejections"_kj, // 2022-10-31 (Monday) |
| 386 | "exportCommonJsDefaultNamespace"_kj, // 2022-10-31 (Monday) |
| 387 | "urlSearchParamsDeleteHasValueArg"_kj, // 2023-07-01 (Saturday) |
| 388 | "brotliContentEncoding"_kj, // 2024-04-29 (Monday) |
| 389 | "cryptoPreservePublicExponent"_kj, // 2023-12-01 (Friday) |
| 390 | "noImportScripts"_kj, // 2024-03-04 (Monday) |
| 391 | "queuesJsonMessages"_kj, // 2024-03-18 (Monday) |
| 392 | "unwrapCustomThenables"_kj, // 2024-04-01 (Monday) |
| 393 | "internalStreamByobReturn"_kj, // 2024-05-13 (Monday) |
| 394 | "blobStandardMimeType"_kj, // 2024-06-03 (Monday) |
| 395 | "fetchStandardUrl"_kj, // 2024-06-03 (Monday) |
| 396 | "cacheOptionEnabled"_kj, // 2024-11-11 (Monday) |
| 397 | "allowCustomPorts"_kj, // 2024-09-02 (Monday) |
| 398 | "internalWritableStreamAbortClearsQueue"_kj, // 2024-09-02 (Monday) |
| 399 | "handleCrossRequestPromiseResolution"_kj, // 2024-10-14 (Monday) |
| 400 | "upperCaseAllHttpMethods"_kj, // 2024-10-14 (Monday) |
| 401 | "noTopLevelAwaitInRequire"_kj, // 2024-12-02 (Monday) |
| 402 | "fixupTransformStreamBackpressure"_kj, // 2024-12-16 (Monday) |
| 403 | "obsolete74"_kj, // 2025-03-10 (Monday) |
| 404 | "cacheApiRequestCfOverridesCacheRules"_kj, // 2025-05-19 (Monday) |
| 405 | "cacheApiCompatFlags"_kj, // 2025-04-19 (Saturday) |
| 406 | "jsWeakRef"_kj, // 2025-05-05 (Monday) |
| 407 | "enableNavigatorLanguage"_kj, // 2025-05-19 (Monday) |
| 408 | "allowEvalDuringStartup"_kj, // 2025-06-01 (Sunday) |
| 409 | "bindAsyncLocalStorageSnapshot"_kj, // 2025-06-16 (Monday) |
| 410 | "throwOnUnrecognizedImportAssertion"_kj, // 2025-06-16 (Monday) |
| 411 | "setEventTargetThis"_kj, // 2025-08-01 (Friday) |
| 412 | "enableForwardableEmailFullHeaders"_kj, // 2025-08-01 (Friday) |
| 413 | "exposeGlobalMessageChannel"_kj, // 2025-08-15 (Friday) |
| 414 | "pythonWorkersForceNewVendorPath"_kj, // 2025-08-11 (Monday) |
| 415 | "enableWorkflowScriptValidation"_kj, // 2025-09-20 (Saturday) |
| 416 | "stripAuthorizationOnCrossOriginRedirect"_kj, // 2025-09-01 (Monday) |
| 417 | "enableCtxExports"_kj, // 2025-11-17 (Monday) |
| 418 | |
| 419 | // Non-conformant dates via impliedByAfterDate |
| 420 | "pythonWorkers"_kj, // 2000-01-01 (Saturday) via impliedByAfterDate |
| 421 | "nodeJsCompatV2"_kj, // 2024-09-23 (Monday) via impliedByAfterDate |
| 422 | "nodeJsZlib"_kj, // 2024-09-23 (Monday) via impliedByAfterDate |
| 423 | "enableNodejsHttpModules"_kj, // 2025-08-15 (Friday) via impliedByAfterDate |
| 424 | "enableNodejsHttpServerModules"_kj, // 2025-09-01 (Monday) via impliedByAfterDate |
| 425 | "removeNodejsCompatEOL"_kj, // 2025-09-01 (Monday) via impliedByAfterDate |
| 426 | "enableNodeJsHttp2Module"_kj, // 2025-09-01 (Monday) via impliedByAfterDate |
| 427 | "removeNodejsCompatEOLv23"_kj, // 2025-09-01 (Monday) via impliedByAfterDate |
| 428 | "enableNodeJsProcessV2"_kj, // 2025-09-15 (Monday) via impliedByAfterDate |
| 429 | "enableNodeJsFsModule"_kj, // 2025-09-15 (Monday) via impliedByAfterDate |
| 430 | "enableNodeJsOsModule"_kj, // 2025-09-15 (Monday) via impliedByAfterDate |
| 431 | "pythonWorkflows"_kj, // 2025-09-20 (Saturday) via impliedByAfterDate |
| 432 | "enableNodeJsConsoleModule"_kj, // 2025-09-21 (Sunday) via impliedByAfterDate |
| 433 | "pythonWorkers20250116"_kj, // 2025-09-29 (Monday) via impliedByAfterDate |
| 434 | "removeNodejsCompatEOLv22"_kj, // 2027-04-30 (Friday) via impliedByAfterDate |
| 435 | "removeNodejsCompatEOLv24"_kj, // 2028-04-30 (Sunday) via impliedByAfterDate |
| 436 | }); |
| 437 | |
| 438 | // Helper function to suggest the next valid date (Tuesday, Wednesday, or Thursday) |
| 439 | auto suggestNextValidDate = |
| 440 | [](const std::chrono::year_month_day& ymd) -> std::chrono::year_month_day { |
| 441 | auto currentDate = std::chrono::sys_days{ymd}; |
| 442 | auto wd = std::chrono::weekday{currentDate}; |
| 443 | |
| 444 | // Calculate days until next Tuesday (2), Wednesday (3), or Thursday (4) |
| 445 | int currentDay = wd.c_encoding(); // 0 = Sunday, 1 = Monday, etc. |
| 446 | int daysToAdd; |
| 447 | |
| 448 | if (currentDay <= 1) { |
| 449 | // Sunday (0) or Monday (1): next valid day is Tuesday |
| 450 | daysToAdd = 2 - currentDay; |
| 451 | } else if (currentDay >= 5) { |
| 452 | // Friday (5) or Saturday (6): next valid day is Tuesday of next week |
| 453 | daysToAdd = (7 - currentDay) + 2; |
| 454 | } else { |
| 455 | // Already Tuesday/Wednesday/Thursday - this shouldn't happen |
| 456 | daysToAdd = 0; |
| 457 | } |
| 458 | |
| 459 | return std::chrono::year_month_day{currentDate + std::chrono::days{daysToAdd}}; |
| 460 | }; |
| 461 | |
| 462 | // Helper function to parse and validate date |
| 463 | auto parseDate = [](kj::StringPtr dateStr) -> kj::Maybe<std::chrono::year_month_day> { |
| 464 | // First validate the date using normalizeCompatDate |
| 465 | KJ_IF_SOME(normalized, normalizeCompatDate(dateStr)) { |
| 466 | // Parse the validated date string |
| 467 | int year, month, day; |
| 468 | KJ_ASSERT(sscanf(normalized.cStr(), "%d-%d-%d", &year, &month, &day) == 3); |
| 469 | |
| 470 | // Create year_month_day |
| 471 | auto ymd = std::chrono::year_month_day{ |
| 472 | std::chrono::year{year}, |
| 473 | std::chrono::month{static_cast<unsigned>(month)}, |
| 474 | std::chrono::day{static_cast<unsigned>(day)}, |
| 475 | }; |
| 476 | KJ_ASSERT(ymd.ok()); |
| 477 | |
| 478 | return ymd; |
| 479 | } else { |
| 480 | return kj::none; |
| 481 | } |
| 482 | }; |
| 483 | |
| 484 | // Check all compatibility flag fields |
| 485 | auto schema = capnp::Schema::from<CompatibilityFlags>(); |
| 486 | auto fields = schema.getFields(); |
| 487 | |
| 488 | kj::Vector<kj::String> violations; |
| 489 | |
| 490 | for (auto field: fields) { |
| 491 | auto fieldName = field.getProto().getName(); |
| 492 | |
| 493 | // Skip if this specific flag is in the allowed exceptions list |
| 494 | if (allowedFlagExceptions.contains(fieldName)) { |
| 495 | continue; |
| 496 | } |
| 497 | |
| 498 | for (auto annotation: field.getProto().getAnnotations()) { |
| 499 | kj::Maybe<kj::StringPtr> maybeDateStr; |
| 500 | |
| 501 | if (annotation.getId() == COMPAT_ENABLE_DATE_ANNOTATION_ID) { |
| 502 | maybeDateStr = annotation.getValue().getText(); |
| 503 | } else if (annotation.getId() == IMPLIED_BY_AFTER_DATE_ANNOTATION_ID) { |
| 504 | auto value = annotation.getValue(); |
| 505 | auto s = value.getStruct().as<workerd::ImpliedByAfterDate>(); |
| 506 | maybeDateStr = s.getDate(); |
| 507 | } |
| 508 | |
| 509 | KJ_IF_SOME(dateStr, maybeDateStr) { |
| 510 | auto ymd = KJ_REQUIRE_NONNULL( |
| 511 | parseDate(dateStr), "Invalid compatibility flag date format: ", dateStr); |
| 512 | auto suggestedYmd = suggestNextValidDate(ymd); |
| 513 | |
| 514 | // If suggestNextValidDate returns a different date, the original date was invalid |
| 515 | if (ymd != suggestedYmd) { |
| 516 | static const char* dayNames[] = { |
| 517 | "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}; |
| 518 | |
| 519 | auto wd = std::chrono::weekday{std::chrono::sys_days{ymd}}; |
| 520 | int dayOfWeek = wd.c_encoding(); |
| 521 | |
| 522 | auto suggestedWd = std::chrono::weekday{std::chrono::sys_days{suggestedYmd}}; |
| 523 | int suggestedDayOfWeek = suggestedWd.c_encoding(); |
| 524 | |
| 525 | auto suggestedDateStr = std::format("{:%F}", suggestedYmd); |
| 526 | violations.add(kj::str("Field '", fieldName, "' has date ", dateStr, " which is a ", |
| 527 | dayNames[dayOfWeek], ". Dates must be Tuesday, Wednesday, or Thursday. ", |
| 528 | "Suggestion: use ", suggestedDateStr.c_str(), " (", dayNames[suggestedDayOfWeek], |
| 529 | ") instead.")); |
| 530 | } |
| 531 | } |
| 532 | } |
| 533 | } |
| 534 | |
| 535 | if (!violations.empty()) { |
| 536 | KJ_FAIL_ASSERT("Compatibility date violations found:\n", kj::strArray(violations, "\n")); |
| 537 | } |
| 538 | } |
| 539 | |
| 540 | } // namespace |
| 541 | } // namespace workerd |