-
Notifications
You must be signed in to change notification settings - Fork 5.3k
filter: Add an incomplete pluggable HTTP caching filter (CacheFilter). #7198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
121 commits
Select commit
Hold shift + click to select a range
55ea9a2
Add 'age' and 'expires' inline headers to support HTTP caching (#868).
toddmgreer f1a89f4
Add an incomplete pluggable HTTP caching filter (CacheFilter). (#868)
toddmgreer d5bb1c5
Add cache/config to BUILD file.
toddmgreer 72f3bea
Fix formatting.
toddmgreer 0611bed
Fix spelling, and expand spellcheck's dictionary.
toddmgreer 3123971
Rename config_lib to config.
toddmgreer 424e5bc
Add some RFC terms to spelling_dictionary.txt.
toddmgreer d77ff8c
Address clang-tidy findings.
toddmgreer b144193
Remove cache_filter_test until it has actual tests to run.
toddmgreer ee2431a
Address first batch of comments. More to come.
toddmgreer f432612
Style cleanups requested in first review, and a few more unit tests.
toddmgreer 1ac7e7a
Comment helper functions.
toddmgreer 3bb2056
Use absl::flat_hash_map instead of std::unordered_map.
toddmgreer f490c86
Fix formatting and spelling.
toddmgreer bfdf8ee
Add scheme checks to isCacheableRequest; misc cleanup.
toddmgreer 1442b8c
Add cache-control parsing TODO, and fix eatLeadingDuration's arg type.
toddmgreer ef53f4c
Fix SystemTime subtraction underflow, and improve tests.
toddmgreer b4ceab6
Add owners.
toddmgreer 45a9f45
Merge remote-tracking branch 'upstream/master'
toddmgreer 7bf20cd
Fix formatting.
toddmgreer 963f433
Merge remote-tracking branch 'upstream/master'
toddmgreer d1b272d
Add an empty api_proto_package to please check_format.
toddmgreer 9231814
Delete redundant absl deps.
toddmgreer a1fdd9b
Merge remote-tracking branch 'upstream/master'
toddmgreer 4812257
Return '{}' instead of 'SystemTime()', per modernize-return-braced-in…
toddmgreer 4ffced5
Prevent creation of an HttpCacheFilter that isn't owned by a shared_p…
toddmgreer 6f3a221
Add CacheFilterFactory tests.
toddmgreer f2bf5cb
Fix formatting.
toddmgreer 828331b
Fix formatting.
toddmgreer eda41c7
Fix formatting, this time with the right version of clang-format.
toddmgreer 56f6434
Add tests for RawByteRange
capoferro 8cc9562
Add tests for AdjustedByteRange.
capoferro 3e16e8d
Label death tests appropriately
capoferro 11e1520
Add assert tests for request headers in LookupRequest
capoferro d076283
Add tests for fallback header 'expired'
capoferro afc4523
Add CacheFilter test.
toddmgreer 969ee2f
Fix incorrect naming style.
toddmgreer 68995c3
Merge commit '81460d8482b73ba70ea15b76163a7c645e2a7b96'
toddmgreer d5edfae
Format fix.
toddmgreer f3161a2
Merge commit '51757dca1df458c78e63cccc6e7d0f357082bff0'
toddmgreer 705c573
Remove death tests and unnecessary suite classes
capoferro 80d3438
Merge pull request #1 from capoferro/raw_byte_range_test
toddmgreer 024a072
Merge branch 'master' into master
toddmgreer 7444101
Add cache filter to HttpFilterNameValues.
toddmgreer db1d528
Use HttpFilterNameValues::Cache in CacheFilterFactory
toddmgreer b72afdf
Make CacheFilter Loggable.
toddmgreer 2697008
Use HeaderMap::ForwardedProto instead of Scheme in cache key, because…
toddmgreer a30a784
Add CacheFilter integration test.
toddmgreer c8dafa0
Fix formatting
toddmgreer 8ff6ae4
Merge branch 'master' of github.com:toddmgreer/envoy
toddmgreer 03e1908
buildozer and spelling fixes
toddmgreer 8d44962
Adapted to upstream changes:
toddmgreer 4d3d57e
Minor cleanups
toddmgreer 84a8532
Change AdjustByteRange to use C++-style half-open intervals instead o…
toddmgreer 463994a
Explain reason to use std::enable_shared_from_this.
toddmgreer 48f046b
Clarify and comment out range handling code that can't yet be reached.
toddmgreer 03d1156
Style fixes.
toddmgreer 473e8fe
Make Cache.name required.
toddmgreer 259b755
Handle unexpected onBody callbacks.
toddmgreer 0097e24
Make adjustByteRangeSet public, with unit tests.
toddmgreer 296a420
Use class instead of namespace in http_cache_utils, per reviewer requ…
toddmgreer e83d95a
Make CacheFilter's internal helper functions static members, per envo…
toddmgreer a0af25b
Add effectiveMaxAge tests.
toddmgreer e5c030e
Merge commit 'eb9e13626830ad46a9c754035db3fa81b32a8f82' into HEAD
toddmgreer a249ff6
Merge commit '2591deacb306ec665812b1cf18fcaee57247ffd1' into HEAD
toddmgreer 3a9406a
Adapt to HeaderMap changes.
toddmgreer 6f91e36
Adapt to proto rules changes.
toddmgreer c1bb3a3
Add security_posture and status.
toddmgreer aaf9ad9
Add placeholder .rst
toddmgreer 2b33348
Move from v3alpha to v2, per htuch.
toddmgreer 39064e8
Fix rst file
toddmgreer aebd8ca
Add to http_filter.rst
toddmgreer d24a316
Fix formatting
toddmgreer 7a05205
Revert accidentally reformatted codec_impl.cc (partial revert of 84a8…
toddmgreer 4d0209f
Merge branch 'master' into master
toddmgreer d50765c
Change query_strings whitelist/blacklist to use oneof, and rename for…
toddmgreer a82a214
Improve config docs.
toddmgreer 34b14c2
Uncomment unimplemented fields in cache.proto, since the formatting t…
toddmgreer 29d50d1
Fix wrong BUILD deps incantation
toddmgreer 02c012f
Add proto migration annotations
toddmgreer 8925c4f
Unify range TODO
toddmgreer eec7683
Add generated BUILD/proto files.
toddmgreer 3bfe116
Fix TODO formatting
toddmgreer f209aef
Use FALLTHRU macro
toddmgreer 12876b3
Simplified HttpTimeTest to make it clear that it checks the results o…
toddmgreer 064e083
Added _ to struct member variable names.
toddmgreer 1782f2c
Add integration test TODO to test unimplemented features.
toddmgreer 9e5c10f
Remove unneeded dictionary addition
toddmgreer cffcc02
Rename envoy::config::filter::http::cache::v2::Cache to envoy::config…
toddmgreer 9f4a1c6
Add overview doc.
toddmgreer c796d4d
Change v3alpha to v3
toddmgreer dc05534
Use api.v2.route.QueryParameterMatcher to specify query params for keys.
toddmgreer 118191f
Use StringMatcher for allowed_vary_headers.
toddmgreer a2a9ae6
Add an overview of writing a cache storage implementation
toddmgreer 0ce2a64
Highlight field names in comments
toddmgreer dd0703f
Comment out TODO comments.
toddmgreer 4304cbc
Only create date format strings once.
toddmgreer 05c0566
simplify decl w/ auto
toddmgreer 1bc5928
Minor doc improvements; use reverse DNS for cache storage implementat…
toddmgreer 6885b82
*-highlight "vary" in comments.
toddmgreer 3f128e3
Correct missed SimpleHttpCache to envoy.extensions.http.cache.simple …
toddmgreer 9b1c68a
Update source/extensions/filters/http/cache/http_cache.h
toddmgreer 4585b14
Addressing review comments from mattklien123
toddmgreer f09822b
Add typed_config, and use getAndCheckFactory.
toddmgreer dbbcbd1
Pass config into cache storage implementations.
toddmgreer 386c18b
Put CacheConfig in v3
toddmgreer adb947e
Elaborated on the effects of allowed_vary_headers.
toddmgreer 1903ae0
Fix test error due to no longer requiring CacheConfig.name
toddmgreer 4f26ac8
Reran tools/proto_format.sh fix
toddmgreer 2028b99
Update v2->v3
toddmgreer 56ebd02
Fix various v2->v3 issues
toddmgreer 8e10a41
bazel build -c fastbuild @envoy_api_canonical//envoy/... now works
toddmgreer 063ba6d
Revert v3 changes
toddmgreer 50d20a6
Revert "Update v2->v3"
toddmgreer fdbc1c5
Revert "Reran tools/proto_format.sh fix"
toddmgreer 353e8a8
Revert "Put CacheConfig in v3"
toddmgreer 4027ab3
Got 'tools/proto_format.sh fix' to run successfully.
toddmgreer 3ebf4f2
Merge remote-tracking branch 'upstream/master'
toddmgreer 1b73cf0
tools/proto_format.sh fix
toddmgreer 62d01c8
Adapt to upstream factory changes
toddmgreer c0223c3
Comment out rst docs about unenabled config fields, to avoid dangling…
toddmgreer File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| # DO NOT EDIT. This file is generated by tools/proto_sync.py. | ||
|
|
||
| load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") | ||
|
|
||
| licenses(["notice"]) # Apache 2 | ||
|
|
||
| api_proto_package( | ||
| deps = [ | ||
| "//envoy/api/v2/route:pkg", | ||
| "//envoy/type/matcher:pkg", | ||
| "@com_github_cncf_udpa//udpa/annotations:pkg", | ||
| ], | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,82 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package envoy.config.filter.http.cache.v2; | ||
|
|
||
| import "envoy/api/v2/route/route_components.proto"; | ||
| import "envoy/type/matcher/string.proto"; | ||
|
|
||
| import "google/protobuf/any.proto"; | ||
|
|
||
| import "udpa/annotations/migrate.proto"; | ||
| import "validate/validate.proto"; | ||
|
|
||
| option java_package = "io.envoyproxy.envoy.config.filter.http.cache.v2"; | ||
| option java_outer_classname = "CacheProto"; | ||
| option java_multiple_files = true; | ||
| option (udpa.annotations.file_migrate).move_to_package = "envoy.extensions.filters.http.cache.v3"; | ||
|
|
||
| // [#protodoc-title: HTTP Cache Filter] | ||
| // HTTP Cache Filter :ref:`overview <config_http_filters_cache>`. | ||
| // [#extension: envoy.filters.http.cache] | ||
toddmgreer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // [#next-free-field: 6] | ||
| message CacheConfig { | ||
| // [#not-implemented-hide:] | ||
| // Modifies cache key creation by restricting which parts of the URL are included. | ||
| message KeyCreatorParams { | ||
| // If true, exclude the URL scheme from the cache key. Set to true if your origins always | ||
| // produce the same response for http and https requests. | ||
| bool exclude_scheme = 1; | ||
toddmgreer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // If true, exclude the host from the cache key. Set to true if your origins' responses don't | ||
| // ever depend on host. | ||
| bool exclude_host = 2; | ||
|
|
||
| // If *query_parameters_included* is nonempty, only query parameters matched | ||
| // by one or more of its matchers are included in the cache key. Any other | ||
| // query params will not affect cache lookup. | ||
| repeated api.v2.route.QueryParameterMatcher query_parameters_included = 3; | ||
|
|
||
| // If *query_parameters_excluded* is nonempty, query parameters matched by one | ||
| // or more of its matchers are excluded from the cache key (even if also | ||
| // matched by *query_parameters_included*), and will not affect cache lookup. | ||
| repeated api.v2.route.QueryParameterMatcher query_parameters_excluded = 4; | ||
| } | ||
|
|
||
| // Config specific to the cache storage implementation. | ||
| google.protobuf.Any typed_config = 1; | ||
|
|
||
| // Name of cache implementation to use, as specified in the intended HttpCacheFactory | ||
| // implementation. Cache names should use reverse DNS format, though this is not enforced. | ||
| string name = 2; | ||
|
|
||
| // [#not-implemented-hide:] | ||
| // <TODO(toddmgreer) implement *vary* headers> | ||
| // | ||
| // List of allowed *Vary* headers. | ||
| // | ||
| // The *vary* response header holds a list of header names that affect the | ||
| // contents of a response, as described by | ||
| // https://httpwg.org/specs/rfc7234.html#caching.negotiated.responses. | ||
| // | ||
| // During insertion, *allowed_vary_headers* acts as a whitelist: if a | ||
| // response's *vary* header mentions any header names that aren't in | ||
| // *allowed_vary_headers*, that response will not be cached. | ||
| // | ||
| // During lookup, *allowed_vary_headers* controls what request headers will be | ||
| // sent to the cache storage implementation. | ||
| repeated type.matcher.StringMatcher allowed_vary_headers = 3; | ||
|
|
||
| // [#not-implemented-hide:] | ||
| // <TODO(toddmgreer) implement key customization> | ||
| // | ||
| // Modifies cache key creation by restricting which parts of the URL are included. | ||
| KeyCreatorParams key_creator_params = 4; | ||
|
|
||
| // [#not-implemented-hide:] | ||
| // <TODO(toddmgreer) implement size limit> | ||
| // | ||
| // Max body size the cache filter will insert into a cache. 0 means unlimited (though the cache | ||
| // storage implementation may have its own limit beyond which it will reject insertions). | ||
| uint32 max_body_bytes = 5; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # DO NOT EDIT. This file is generated by tools/proto_sync.py. | ||
|
|
||
| load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") | ||
|
|
||
| licenses(["notice"]) # Apache 2 | ||
|
|
||
| api_proto_package( | ||
| deps = [ | ||
| "//envoy/config/filter/http/cache/v2:pkg", | ||
| "//envoy/config/route/v3:pkg", | ||
| "//envoy/type/matcher/v3:pkg", | ||
| "@com_github_cncf_udpa//udpa/annotations:pkg", | ||
| ], | ||
| ) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| syntax = "proto3"; | ||
|
|
||
| package envoy.extensions.filters.http.cache.v3; | ||
|
|
||
| import "envoy/config/route/v3/route_components.proto"; | ||
| import "envoy/type/matcher/v3/string.proto"; | ||
|
|
||
| import "google/protobuf/any.proto"; | ||
|
|
||
| import "udpa/annotations/versioning.proto"; | ||
|
|
||
| import "validate/validate.proto"; | ||
|
|
||
| option java_package = "io.envoyproxy.envoy.extensions.filters.http.cache.v3"; | ||
| option java_outer_classname = "CacheProto"; | ||
| option java_multiple_files = true; | ||
|
|
||
| // [#protodoc-title: HTTP Cache Filter] | ||
| // HTTP Cache Filter :ref:`overview <config_http_filters_cache>`. | ||
| // [#extension: envoy.filters.http.cache] | ||
|
|
||
| // [#next-free-field: 6] | ||
| message CacheConfig { | ||
| option (udpa.annotations.versioning).previous_message_type = | ||
| "envoy.config.filter.http.cache.v2.CacheConfig"; | ||
|
|
||
| // [#not-implemented-hide:] | ||
| // Modifies cache key creation by restricting which parts of the URL are included. | ||
| message KeyCreatorParams { | ||
| option (udpa.annotations.versioning).previous_message_type = | ||
| "envoy.config.filter.http.cache.v2.CacheConfig.KeyCreatorParams"; | ||
|
|
||
| // If true, exclude the URL scheme from the cache key. Set to true if your origins always | ||
| // produce the same response for http and https requests. | ||
| bool exclude_scheme = 1; | ||
|
|
||
| // If true, exclude the host from the cache key. Set to true if your origins' responses don't | ||
| // ever depend on host. | ||
| bool exclude_host = 2; | ||
|
|
||
| // If *query_parameters_included* is nonempty, only query parameters matched | ||
| // by one or more of its matchers are included in the cache key. Any other | ||
| // query params will not affect cache lookup. | ||
| repeated config.route.v3.QueryParameterMatcher query_parameters_included = 3; | ||
|
|
||
| // If *query_parameters_excluded* is nonempty, query parameters matched by one | ||
| // or more of its matchers are excluded from the cache key (even if also | ||
| // matched by *query_parameters_included*), and will not affect cache lookup. | ||
| repeated config.route.v3.QueryParameterMatcher query_parameters_excluded = 4; | ||
| } | ||
|
|
||
| // Config specific to the cache storage implementation. | ||
| google.protobuf.Any typed_config = 1; | ||
|
|
||
| // Name of cache implementation to use, as specified in the intended HttpCacheFactory | ||
| // implementation. Cache names should use reverse DNS format, though this is not enforced. | ||
| string name = 2; | ||
|
|
||
| // [#not-implemented-hide:] | ||
| // <TODO(toddmgreer) implement *vary* headers> | ||
| // | ||
| // List of allowed *Vary* headers. | ||
| // | ||
| // The *vary* response header holds a list of header names that affect the | ||
| // contents of a response, as described by | ||
| // https://httpwg.org/specs/rfc7234.html#caching.negotiated.responses. | ||
| // | ||
| // During insertion, *allowed_vary_headers* acts as a whitelist: if a | ||
| // response's *vary* header mentions any header names that aren't in | ||
| // *allowed_vary_headers*, that response will not be cached. | ||
| // | ||
| // During lookup, *allowed_vary_headers* controls what request headers will be | ||
| // sent to the cache storage implementation. | ||
| repeated type.matcher.v3.StringMatcher allowed_vary_headers = 3; | ||
|
|
||
| // [#not-implemented-hide:] | ||
| // <TODO(toddmgreer) implement key customization> | ||
| // | ||
| // Modifies cache key creation by restricting which parts of the URL are included. | ||
| KeyCreatorParams key_creator_params = 4; | ||
|
|
||
| // [#not-implemented-hide:] | ||
| // <TODO(toddmgreer) implement size limit> | ||
| // | ||
| // Max body size the cache filter will insert into a cache. 0 means unlimited (though the cache | ||
| // storage implementation may have its own limit beyond which it will reject insertions). | ||
| uint32 max_body_bytes = 5; | ||
| } | ||
toddmgreer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
46 changes: 46 additions & 0 deletions
46
docs/root/configuration/http/http_filters/cache_filter.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| .. _config_http_filters_cache: | ||
toddmgreer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| HTTP Cache Filter | ||
| ================= | ||
| .. attention:: Work in Progress--not ready for deployment | ||
|
|
||
| HTTP caching can improve system throughput, latency, and network/backend load | ||
| levels when the same content is requested multiple times. Caching is | ||
| particularly valuable for edge proxies and browser-based traffic, which | ||
| typically include many cacheable static resources, but it can be useful any time | ||
| there is enough repeatedly served cacheable content. | ||
|
|
||
| Configuration | ||
| ------------- | ||
| * :ref:`Configuration API <envoy_api_msg_config.filter.http.cache.v2.cacheConfig>` | ||
| * This filter should be configured with the name *envoy.filters.http.cache*. | ||
|
|
||
| The only required configuration field is :ref:`name | ||
| <envoy_api_field_config.filter.http.cache.v2.CacheConfig.name>`, which must | ||
| specify a valid cache storage implementation linked into your Envoy | ||
| binary. Specifying 'envoy.extensions.http.cache.simple' will select a proof-of-concept | ||
| implementation included in the Envoy source. More implementations can (and will) | ||
| be provided by implementing Envoy::Extensions::HttpFilters::Cache::HttpCache. To | ||
| write a cache storage implementation, see :repo:`Writing Cache Filter | ||
| Implementations <source/docs/cache_filter_plugins.md>` | ||
|
|
||
| .. TODO(toddmgreer) Describe other fields as they get implemented. | ||
| The remaining configuration fields control caching behavior and limits. By | ||
| default, this filter will cache almost all responses that are considered | ||
| cacheable by `RFC7234 <https://httpwg.org/specs/rfc7234.html>`_, with handling | ||
| of conditional (`RFC7232 <https://httpwg.org/specs/rfc7232.html>`_), and *range* | ||
| (`RFC7233 <https://httpwg.org/specs/rfc7233.html>`_) requests. Those RFC define | ||
| which request methods and response codes are cacheable, subject to the | ||
| cache-related headers they also define: *cache-control*, *range*, *if-match*, | ||
| *if-none-match*, *if-modified-since*, *if-unmodified-since*, *if-range*, *authorization*, | ||
| *date*, *age*, *expires*, and *vary*. Responses with a *vary* header will only be cached | ||
| if the named headers are listed in :ref:`allowed_vary_headers | ||
| <envoy_api_field_config.filter.http.cache.v3.CacheConfig.allowed_vary_headers>` | ||
|
|
||
| Status | ||
| ------ | ||
| * This filter *is* ready for developers to write cache storage plugins; please | ||
| contribute them to the Envoy repository if possible. | ||
| * This filter *is* ready for contributions to help finish its implementation of | ||
| HTTP caching semantics. | ||
| * This filter *is not* ready for actual use. Please see TODOs in the code. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
13 changes: 13 additions & 0 deletions
13
generated_api_shadow/envoy/config/filter/http/cache/v2/BUILD
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
82 changes: 82 additions & 0 deletions
82
generated_api_shadow/envoy/config/filter/http/cache/v2/cache.proto
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.