Skip to content

"_ngcontent" inserted into font-url, making it invalid #38587

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
mhombach opened this issue Aug 26, 2020 · 2 comments
Closed

"_ngcontent" inserted into font-url, making it invalid #38587

mhombach opened this issue Aug 26, 2020 · 2 comments

Comments

@mhombach
Copy link

mhombach commented Aug 26, 2020

🐞 bug report

Affected Package

Seems to be related to SCSS-compiling but seems to be also related to angular itself.

Is this a regression?

It seems this bug is also existing in version 8.2.3 / 8.3.0, most likely in all versions inbetween.

Description

If an angular project uses SCSS and imports a font by url, in some cases angular wrongly inserts a [_ngcontent-...] into the url so the font cannot be loaded.
See reproduction section for details.

🔬 Minimal Reproduction

Created a reproduction repo: https://github.com/mhombach/angularBugReport1
But basically the one step to reproducer is to add the following line into your SCSS file:

@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');

If you load this app and look into the network inspector you will see a 400 error for loading the font and the reason is that the above import-url is changed to:
https://fonts.googleapis.com/css2?family=Roboto:wght@400;500[_ngcontent-kph-c11];700&display=swap
This url is btw the official url that google-fonts is giving you for embedding.

🔥 Exception or Error

There is no error other than the browser indication the 400.

🌍 Your Environment

Angular Version:


Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.1000.7
@angular-devkit/build-angular     0.1000.7
@angular-devkit/build-optimizer   0.1000.7
@angular-devkit/build-webpack     0.1000.7
@angular-devkit/core              10.0.7
@angular-devkit/schematics        10.0.7
@angular/cli                      10.0.7
@ngtools/webpack                  10.0.7
@schematics/angular               10.0.7
@schematics/update                0.1000.7
rxjs                              6.5.5
typescript                        3.9.7
webpack                           4.43.0

Anything else relevant?
I tested some patterns that don't produce this error.
Those lines are working:

@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@500;700&display=swap');

I think this error is produced when there are more then 2 weight-numbers separated by a semicolon.

Update:
This import line here

@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@500;700&family=Roboto:wght@400;500&display=swap');

produces this url

https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@500;700&family=Roboto[_ngcontent-muh-c11]:wght@400;500&display=swap

so this time the insert happens on a different place.

@sonukapoor sonukapoor added area: core Issues related to the framework runtime core: styling bindings labels Aug 26, 2020
@ngbot ngbot bot modified the milestone: needsTriage Aug 26, 2020
@mhombach
Copy link
Author

mhombach commented Sep 2, 2020

I dived into the code, debugged and i found the problem, but I am not 100% which part should be changed because i have 0 knowledge in the angular internal mechanisms. So i am describing my findings here. Maybe some of you have already a solution in mind.

I very slightly adjusted the repo mention in the description: I changed the SCSS code of the app-component to the following:

@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap');

div {
    color: red;
}

Findings

The problem lies in the compiler.umd.js file ( @angular/compiler/bundles).
There you can find the following line.

function processRules(input, ruleCallback) {

This stringreplace-function receives the full SCSS string and applies escapeBlocks(input) on it, which produces the following object;

StringWithEscapedBlocks {
  escapedString: '@import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap");\n' +
    'div {%BLOCK%}\n',
  blocks: [ '\n  color: red;\n' ]
}

So far the import-line is still correct.
Now the following regex is applied on it:

/(\s*)([^;\{\}]+?)(\s*)((?:{%BLOCK%}?\s*;?)|(?:\s*;))/g

This regex splits the import-line wrongly. It generates the following pieces:

  • @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;
  • 500;
  • 700&display=swap");
  • \ndiv {%BLOCK%}\n

Yes, my first intuition was that the regex has some problem.
But on my way of finding this regex, i passed through the ShadowCss.prototype._scopeSelectors function (which internally then calls the processRules-function).
In this function there are several checks and actions on substrings like

  • @
  • @media
  • @supports
  • @page
  • @document

So my guess is that those are kind of exceptions so that those rules are not processed the same way as "normal" rules so they maybe don't get corrupted?
But the context of this extra-checking is the callback of the processRules-function so the order of execution is:

  1. apply processRules(), which corrupts the import-line by the regex
  2. do some checking if the rule starts with @something

So my theory of having some pre-checking there is wrong i guess.

So this would only leave my initial guess that it is correct that the regex is applied to every rule but there is some bug in the regex itself which breaks certain import-lines apart (the rest of the logic then treats each segment as css-rule and so makes wrong followup-errors based on those segments).

I'm not good enough at regex to fix this sadly.
Also i am wondering if it might be maybe a better approach to exclude lines starting with @import from the beginning, because there is no reason, im my eyes, to modify those lines ever.

Maybe one of the more angular-experienced devs can enlighten me here :)

I hope that this finding is helpfull.

Regarding the severity of this bug i was having thoughts, because it seems that this bug is existing since multiple major-versions and i have a gut feeling that there are some angular-applications out there where fonts can't be loaded because of this bug but the devs are not checking the network-log but only the console log and so maybe don't even realize that they are using slightly wrong fonts :/

crisbeto added a commit to crisbeto/angular that referenced this issue Sep 4, 2020
…d semicolons

The style encapsulation logic currently uses a regex to parse CSS style
declarations out of a rule. The problem is that the current regex is a bit too
broad in that it matches any valid characters preceeded by a selector
and suffixed with a semicolon, which breaks down for `@import` statements
that have a URL containing semicolons (e.g. the the URLs provided by
Google Fonts like https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap).

These changes resolve the issue by making the regex a bit more narrow in what it's supposed to match.

Fixes angular#38587.
@crisbeto crisbeto self-assigned this Sep 4, 2020
crisbeto added a commit to crisbeto/angular that referenced this issue Sep 5, 2020
…d semicolons

At a high level, the current shadow DOM shim logic works by escaping the content of a CSS rule
(e.g. `div {color: red;}` becomes `div {%BLOCK%}`), using a regex to parse out things like the
selector and the rule body, and then re-adding the content after the selector has been modified.
The problem is that the regex has to be very broad in order capture all of the different use cases,
which can cause it to match strings suffixed with a semi-colon in some places where it shouldn't,
like this URL from Google Fonts `https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap`.
Most of the time this is fine, because the logic that escapes the rule content to `%BLOCK%` will
have converted it to something that won't be matched by the regex. However, it breaks down for rules
like `@import` which don't have a body, but can still have quoted content with characters that can
match the regex.

These changes resolve the issue by making a second pass over the escaped string and replacing all
of the remaining quoted content with `%QUOTED%` before parsing it with the regex. Once everything
has been processed, we make a final pass where we restore the quoted content.

In a previous iteration of this PR, I went with a shorter approach which narrowed down the
regex so that it doesn't capture rules without a body. It fixed the issue, but it also ended
up breaking some of the more contrived unit test cases. I decided not to pursue it further, because
we would've ended up with a very long and brittle regex that likely would've broken in even weirder
ways.

Fixes angular#38587.
crisbeto added a commit to crisbeto/angular that referenced this issue Oct 6, 2020
…d semicolons

At a high level, the current shadow DOM shim logic works by escaping the content of a CSS rule
(e.g. `div {color: red;}` becomes `div {%BLOCK%}`), using a regex to parse out things like the
selector and the rule body, and then re-adding the content after the selector has been modified.
The problem is that the regex has to be very broad in order capture all of the different use cases,
which can cause it to match strings suffixed with a semi-colon in some places where it shouldn't,
like this URL from Google Fonts `https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap`.
Most of the time this is fine, because the logic that escapes the rule content to `%BLOCK%` will
have converted it to something that won't be matched by the regex. However, it breaks down for rules
like `@import` which don't have a body, but can still have quoted content with characters that can
match the regex.

These changes resolve the issue by making a second pass over the escaped string and replacing all
of the remaining quoted content with `%QUOTED%` before parsing it with the regex. Once everything
has been processed, we make a final pass where we restore the quoted content.

In a previous iteration of this PR, I went with a shorter approach which narrowed down the
regex so that it doesn't capture rules without a body. It fixed the issue, but it also ended
up breaking some of the more contrived unit test cases. I decided not to pursue it further, because
we would've ended up with a very long and brittle regex that likely would've broken in even weirder
ways.

Fixes angular#38587.
@atscott atscott closed this as completed in 7f689a2 Oct 9, 2020
atscott pushed a commit that referenced this issue Oct 9, 2020
…d semicolons (#38716)

At a high level, the current shadow DOM shim logic works by escaping the content of a CSS rule
(e.g. `div {color: red;}` becomes `div {%BLOCK%}`), using a regex to parse out things like the
selector and the rule body, and then re-adding the content after the selector has been modified.
The problem is that the regex has to be very broad in order capture all of the different use cases,
which can cause it to match strings suffixed with a semi-colon in some places where it shouldn't,
like this URL from Google Fonts `https://fonts.googleapis.com/css2?family=Roboto:wght@400;500&display=swap`.
Most of the time this is fine, because the logic that escapes the rule content to `%BLOCK%` will
have converted it to something that won't be matched by the regex. However, it breaks down for rules
like `@import` which don't have a body, but can still have quoted content with characters that can
match the regex.

These changes resolve the issue by making a second pass over the escaped string and replacing all
of the remaining quoted content with `%QUOTED%` before parsing it with the regex. Once everything
has been processed, we make a final pass where we restore the quoted content.

In a previous iteration of this PR, I went with a shorter approach which narrowed down the
regex so that it doesn't capture rules without a body. It fixed the issue, but it also ended
up breaking some of the more contrived unit test cases. I decided not to pursue it further, because
we would've ended up with a very long and brittle regex that likely would've broken in even weirder
ways.

Fixes #38587.

PR Close #38716
@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Nov 9, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

4 participants