-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
Create a mechanism to expose unstable APIs to plugins #66197
Comments
This sounds like the means for a great Make proposal. |
This might be worth feedback from a broader audience: @WordPress/gutenberg-core |
The background for the introduction of the private APIs in place of My concern with allowing plugin to opt-in to the private APIs is that WordPress would end up in a similar situation again. I worry that we'd end up in a situation in which WordPress couldn't unlock an API for risk of breaking sites (@adamziel worked on the code, so this may not be an issue -- I suggested it & try to track string changes each release).
gutenberg/packages/private-apis/src/implementation.js Lines 62 to 63 in 3aa19d2
@jsnajdr is your request to allow opt-in to be easier only when running the Gutenberg plugin, or do you wish to be able to opt in for sites running WordPress Core? |
I feel this need everyday when developing on Gutenberg. For a long term and sustainable project, a way to have a feedback loop for APIs, just like we do for features is needed. I know it's something that WordPress never had (aside from a small period where we had the experimental APIs in Gutenberg). I don't have a solution, but it's definitely a problem that is worth solving if we want to be shipping the right stable APIs. |
But isn't the consent string a way to opt in? |
@draganescu Right now, the consent string can only be used by Core packages, not third-party packages. At least, that's the theory, they can pretend that they're a core package and use it but that's a hack really and can break anytime if the core package in question start loading in the page. |
Then the 1st thing that I can think of is to properly define the difference between opting into an API to participate into its formation vs relying on an unstable API. |
A way to statically anyalyze the usage of the private APIs would also be good. For most of the experimental APIs we've resorted to using https://www.wpdirectory.net/ to search whether plugins are using an API, but it's not perfect, it can overmatch or be hard to find exact matches. If there were a manifest or something that plugins had to use to say which private APIs they're using it'd be much easier to search for usage and even notify them when we're removing a private API. Not sure how this would work, part of the problem is that an API can be so many things (function, prop, parameter, property etc ...) and there are often different techniques used for each. |
Introducing stage in the lifecycle of a new/experimental API would be good for consumers to understand its maturity. This change could also encourage community feedback, which is ultimately the enabler for stabilizing something. When it comes to usage of experimental APIs, it's clear that the risks are different depending on where the API is in its lifecycle. Using an API that is closer to becoming stable should in theory be less risky. What is a risk? Breaking changes or full removal of API. I think this information about API maturity could speak more to developers about the type of involvement they could have and the risks/limits that come with it. I agree coming up with these stages could be tricky, and there might be downsides to this approach, but it would allow for more flexibility and predictability then now. Good changes from the status quo are to:
|
@youknowriad I haven't been able to think of anything for WordPress Core but for sites running the Gutenberg plugin, it might be worth making the opt-in string static so site's using the APIs can safely do so while testing with the plugin.
Extenders making use of it would need to do a little error catching during the transition period but it's a solvable problem. |
Warning: left field idea incoming... Could the experimental page be built out as a place to document/track/solicit feedback for experimental APIs? It might be as feature-rich as the plugins page, with update/alert badges. Automation would be ideal, e.g., a script could scrape the JS code base for relevant data, which the page could publish, and some other flag in experimental PHP code (under 🤷🏻 |
@peterwilsoncc I didn't realize that we could distinguish between running and not running the Gutenberg plugin when I was writing down this issue. And yes, it could be a very good solution. If a plugin wants to use unstable APIs, it's typically for an experimental feature that can be turned on/off in plugin settings. For example, WooCommerce has a Settings/Advanced/Features screen where you can enable a beta product editor: A new additional constraint would be that this beta editor can be active only when the Gutenberg plugin is installed and active. Today the purpose of the Gutenberg plugin is to provide a bi-weekly "technology preview" of what is coming to Core in the next release. And now it could also provide a "beta environment" to plugins that also want to offer their own "technology preview" features. It all fits together very well. If you want to ship a feature to the general public, it won't be able to use the experimental APIs. The APIs need to be stabilized in Core before they can be used. That's certainly a limitation, but not a bad one. How would the API for opting into the private APIs look like? Currently Core modules can do this: __dangerousOptInToUnstableAPIsOnlyForCoreModules( 'I acknowledge...', '@wordpress/blocks' ); where there is a consent string that serves as a sort of password, and the module also needs to specify its name. Every module name can be used only once, and there is an allowlist of them. That partially prevents non-Core modules from impersonating as a Core module. For experimental API access from plugins we could have a second function: __dangerousOptInToUnstableAPIs( 'I confirm...' ); This function implementation would check How does that sound? @ralucaStan @gigitux @lysyjan would it satisfy the requirements for Woo and MailPoet plugins? It would be also nice to have the private APIs documented and sorted into various stages. That could be a well-maintained |
@jsnajdr Just noting that we actually already do that. We already use So I had assumed the discussion was more general, about Core as well. Also, I do think there's value in improving our communication around the different stages of the different APIs. |
This is not always true. For example, currently, we're using some private APIs for the Customize Your Store project that it is in production and belongs to the Woo onboarding flow. I shared some private APIs that we're using:
From a quick search, it looks like that we're using 20 times We can do this because these packages aren’t imported directly from WordPress Core; instead, we use the npm version.
While I fully agree with our commitment to maintaining stable APIs, I’m not sure why we should take responsibility for not breaking plugins that use private APIs. By definition, these APIs are experimental, and it is the responsibility of plugin developers to ensure their plugins remain compatible with new versions of WordPress. As a platform, we should make this “contract” clear, and we should certainly communicate the status of private APIs more effectively and work towards stabilizing those that have been in use for several years and are relied upon by multiple plugins. |
If a production project needs private APIs, the first question is why these APIs are private and if it's still justified. There are many that could be stabilized right away. Your first example uses the The second example uses the
In my view the API stability policy is there also to protect the user from us engineers 🙂 If a plugin tries to use some API that's no longer there or is different, the user's site will break and the user will be harmed. Then various groups of engineers can argue with each other about whose fault it is and who is responsible, but that doesn't help the user much -- their site is broken. |
Yes, maybe we don't really need much more than that. In its current form, If I make an analogy with
We can discuss also Core and access to private APIs without the Gutenberg plugin active. It's just that we don't have any nice solution for that yet, and also it's not clear who needs such a Core access and what the requirements are. |
For me the need is that we need a better feedback loop with extenders and Extenders can give us that feedback unless they ship their plugins to real users without the need for an additional Gutenberg plugin forced onto their users. It's true that we've managed so far to have something "working" without it but it's far from ideal. |
It seems like we're discussing two things in this issue:
On the 1st point, as I understand it Gutenberg could remove or introduce a breaking change to a private API at any point in time (please correct me if I'm wrong here). This makes it unsafe for plugins like WooCommerce to consume these APIs and subsequently difficult to accomplish point 2 for Gutenberg to get feedback on these APIs. It might also be dangerous relying on the Gutenberg plugin being active as a means to expose private APIs. This seems like it's introducing a dependency that can't be fully controlled by the consuming plugin. For example, one plugin might require version X and the other requires version Y, with version X having the private API that plugin A needs, while breaking changes are made in version Y. Using NPM would be a simple solution that would allow plugins to pin versions with the private APIs without introducing breaking changes in upcoming versions. However, this eliminates the benefits of exposing packages on the I wonder if there's an opportunity to create a minimal package that houses the private APIs and could be pinned by consumers. However, in practice I think there might be too many interdependencies for those private APIs, creating the same performance issues we were trying to avoid. |
I wanted to echo my comment from another thread. The consent string seems like an obvious thing to look at, but I think it's just a symptom of a deeper challenge with the dependency graph. Here's my understanding of the situation:
Does that sound right? If yes, that last steps effectively replaces the dependency graph used by One solution would be for all the dataviews-reliant plugins to require the Gutenberg plugin after all. Then, instead of bundling the package we could perform the substitution to |
The idea of using the presence of the Gutenberg plugin to expose unstable APIs so plugins can get ahead, try, provide feedback, etc, is how we've been thinking about it already. Gutenberg has become, as a matter of fact, a channel for early testing of both user and developer features. Plugins that have a dependency on Gutenberg would be well aware of it and should help promote APIs that become solid to be part of Core releases. I also agree with @youknowriad that it'd be good to formalize the state of some of these, whether that is through "stages" or something else. |
@adamziel The The only problem with This discussion is mainly about packages that i) are part of the platform, and ii) expose private experimental APIs that often change. If plugins could use these private APIs freely, an upgrade from WordPress 6.6 to 6.7 would likely break many sites. |
@youknowriad @mtias I have 2 concerns with requiring the Gutenberg plugin to be active to use these newer APIs:
I think this point might signal that the underlying API should be marked as stable, but I'm not sure that a single plugin (Woo) can make that decision. Even once that decision is made, it means that we need to always install Gutenberg with at least the next 2 WP versions.
This seems like it might wreak havoc on consuming plugins relying on an API being present in a specific version of Gutenberg. |
I think this:
contradicts this:
@jsnajdr isn't that an implicit dependency on a specific WordPress / Gutenberg version? |
I couldn't agree more with this. Making it easier to opt-in when running Gutenberg (via a fixed string or some other means) would help make this feedback loop easier.
The private APIs are, by definition, not ready for use in production. If a theme or plugin chooses to I do accept that sometime we (being WordPress Core developers) could sometime mark APIs stable more quickly but it needs to be done through an issue to allow for folks to discuss whether the API is, in fact, in good shape to be marked stable. I DO want to make it easier for third party developers to assist with the stabalization of the APIs and making it easier for devs to use the APIs in non-production code will help open the feedback loop. What I don't want to do is end up back were we started of experimental APIs been used so widely in production code that WordPress Core gets stuck with the first or second draft of the API by making them available without running the Gutenberg plugin. |
@peterwilsoncc I understand the conundrum and why we're moving this direction. However, if we're wanting to get early feedback on these APIs, I think this method might present some challenges to plugins. As a 3PD, needing to install an additional plugin for a feature to work is less than ideal, but not a deal breaker. Needing to install a plugin for a feature to work AND not knowing if the (latest) installed version will even house the private APIs that allow said feature to work is a dealbreaker. Unless I'm misunderstanding something, without a way to pin a specific version of the plugin (i.e., with the required APIs), there's not a way to guarantee that a feature is supported at any given time. I know that WooCommerce will not be able to reliably use or provide feedback on any of these APIs if that's the case. |
One more question I have if we opt for the plugin approach is how we safe guard against changes in a consumer plugin. Do we need to have strict version checks to make sure the functions work as expected in the event a function is removed or signature changes? if ( GUTENBERG_VERSION === '19.3.0' ) {
doPrivateApiThing();
} |
@adamziel The contradiction can be resolved by distinguishing between:
|
@joshuatf I can offer two arguments that could address your concerns: The private APIs wouldn't change or disappear abruptly and randomly. The changes can be discussed, prepared for, you should be able to detect whether you're dealing with the old or new version. The point is that for stable APIs, we guarantee that plugin authors don't need to do any extra work. Once a stable API is there, WordPress guarantees that it will continue to work exactly the same way, practically forever. But unstable API is not the exact opposite of that (random chaos) but rather that the plugin developer is expected to keep up with the latest changes and update their code. Second, in the future we should have only very few permanently private APIs, if any. They should get stable in a reasonable amount of time. If a plugin succesfully implements a production-ready feature using a private API, that's a strong stabilizing force that creates pressure on Core to stabilize that API, so that plugins can ship a stable version of the feature to general public. That's one thing I hope that this proposal achieves: that the private status will always be only temporary, because we'll have a good process how to get new APIs widely used, get feedback, and improve them quickly. |
I started working on the private API docs in #66558. |
Requiring the presence of Gutenberg plugin to expose Private APIs is a good solution that doesn't work for the business demands of a plugin like WooCommerce whose features are often shipped under deadlines imposed either internally or by contractual obligations. The ideal scenario would be to rely on semver via NPM, but as mentioned above by @joshuatf, this is costly on performance. What if there is a solution that offers the security of semver but forces developers to be respectful of changing APIs? What if the opt-in string was per package and required pinning to a major version of that package?__dangerousOptInToUnstableAPIsOnlyForCoreModules(
'I acknowledge private features are not for use in themes or plugins and doing so will break in the next version of WordPress.',
'@wordpress/router',
'1.10.0^',
'woocommerce'
);
|
This seems to be a project management issue about priorities. If you have a contractual obligation to deliver something by a certain date, then obviously you want to implement that with stable APIs, and you cannot spend time trying out experimental APIs. For stabilizing Core APIs, the overwhelming constraint is "quality": we need to be sure that the API is right and that we can commit to it long-term. Deadlines play a very small role in that process. If another project's major constraint is "time", that leads to problems. It's a classic project management "triangle" where a project is constrained by resources, time, scope and quality and you can improve one metric only by compromising on others.
If I understand it correctly, this means that the opt-in call can fail. It can return an error saying either that the Gutenberg plugin is required and you don't have it, or that you're requesting a version of the package that is not available on this particular WordPress installation. Then you'd have to implement some fallback using stable APIs. Having two versions of a feature, one stable and conservative, and the other is experimental and optional. |
Thanks @jsnajdr
Thats a good point. For the sake of the discussion best to avoid that perspective.
Yes, thats exactly right. The failure is by design what would keep extension developers honest about using an unstable API. WooCommerce, for example, has an L-1 support policy so that only the current latest WP and the previous versions are supported. In this scenario lets say a Private API gets a major version bump, requiring all opted-in consumers to update their consent strings otherwise fatal errors will occur. WooCommerce is now greatly incentivised to proactively address the update, determine what code changes are required for the updated API, and finally handle the situation to avoid breakage. This would be a regular part of the development process and provide enough friction to dissuade lazy usage of private APIs. We'd need to gracefully handle failed consent in the UI by showing a message to update WooCommerce. Unit testing with pre-release versions of Core WP can offer enough time to address issues. Also, new WooCommerce features based on unstable APIs are often hidden behind feature flags where merchants can opt in to the new experience. This allows for early feedback, which affects usage of unstable APIs and ultimately provides feedback to those APIs and enhancing stability. |
@jsnajdr I'm very grateful for this discussion. Thank you for starting it! The idea of stages is great, and having information about how close to stabilization an API is would help us avoid touching fresh or temporarily unstable APIs and encourage us to experiment with the APIs that are close to becoming stable. In MailPoet, we are working on the email editor, and we use a couple of private APIs. The editor is currently behind a feature flag, but we are working towards preparing it for a release. There is no hard deadline, but we would like to release it in the next couple of months. So, there is a motivation to help stabilize the APIs we use.
The stages will give us a clue about the stability of an API, but what about the process for stabilization? As I mentioned, we use private APIs, and we have the motivation to help stabilize them. What are the steps we should take to provide feedback and ask for stabilization? For example, we use As for private APIs, they are available only when Gutenberg is active. I'm thinking about a situation when a private API becomes stable, and it may take a couple of months until it makes it to WP Core. I think it is a bit of a shame because if we know the API is stable and the private version in WP Core is the same, I think we could release the feature to users earlier. |
Yes, in this case open an issue or even a PR that adds the prop to |
Ideally we should discuss, maybe in a separate discussions, what is the best way for developers to submit proposals, how to advocate for API changes, what makes a valid proposal/feature request, etc. While reading about React's release channels and how they expose experimental API, I came across their RFC system https://github.com/reactjs/rfcs. React RFCs (Request for Comments) are a structured way for the React team and community to propose and discuss changes or additions to the React framework. What I like about this process is that:
|
Hey all 👋 I'm going to join the conversation and share / recap a few reflections that I had over the past days with @mirka, @jsnajdr, @tyxla and @jameskoster in the context of tidying up the Currently, some APIs are named "experimental" but that can't be assumed as such, given that the code was released in past WordPress core versions. In parallel, the introduction of the private API mechanism allowed us to develop and iterate on new APIs — with all the consequences and nuances discussed above. Therefore, as @jsnajdr also mentioned above, we are considering introducing the concept of stages. While there is consensus over the "initial" stage ("just started, don't use") and the "stable" stage (stable API, feel free to use), we'd like to gather more feedback on the remaining stages:
|
I come to this conversation trying to understand how to make DataViews avaliable to extenders, while not making it stable yet. Phases sound nice, so I tried to anchor this idea on specifics:
A key question for the "Candidate" phase is whether it requires Gutenberg or it also works with WordPress:
I think I'd go with the 2 scenario. Tying "candidate" to Gutenberg is more favorable for core but it can be too restrictive for extenders — reducing the value we'll get from introducing this phase (feedback from extenders). |
I'd prefer not to split the experimental stage, unless it is purely a procedural difference.
This way, we can make a pretty straightforward decision on when it graduates from alpha to beta — no value judgments on "stability" per se. I wouldn't want extenders to assume that the beta stage will somehow have less breaking changes than the alpha. Upholding that expectation would be costly. |
I understand being reluctant to have two experimental stages, but with a single one, we don't solve the problem for a primary consumer group: 3rd party plugin developers. And to me, that's the biggest and most important group we need to make this mechanism work for. Inside Gutenberg, we can still use both experimental APIs and private ones, so we're not really facing a problem. The real problem is the requirement of having Gutenberg installed - for plugins that can't guarantee that Gutenberg (the plugin) will be available. So, with a single experimental stage, considering WP's BC expectations, third-party devs will still have to wait until an API is stable before using it, right? I think if that's the case, it defeats the purpose of having that mechanism in the first place. To me, the most important reason in favor of a second experimental phase is to actually be able to make it part of WordPress, so it can be used by extenders. I expect this will be documented as a specific exception to the pre-existing BC guidelines, one where plugin developers intentionally use the experimental/private API in a careful way where they cover cases where APIs are missing or are different, and they're responsible for any potential breakages that occur based on any API changes. In my view, the grand purpose behind this mechanism is to provide a vehicle for plugin developers to be early adopters of features and APIs (even if they're not stable just yet) and to contribute upstream with feedback and development, while taking a core-first approach in their product development strategies. Isn't that what the original issue aims to achieve, after all? |
Don't we already have three phases? Phase One: Locked in Gutenberg From my perspective, we simply need to formalize and "stand behind" the existing "phases" as the mechanism which 3rd party developers can rely on for feature availability. WordPress should lean more towards feature plugins as the distribution mechanism for early access. It'd be useful to explore what options we could provide for more visibility of feature plugins and features they expose for third-party use. For example, what if there was a programmatic mechanism within WP for plugins to request the install of an official feature plugin (such as Gutenberg)? Then plugins like WooCommerce could use that mechanism to install Gutenberg if a user enables a flag opting-in to a new feature that is currently dependent on Gutenberg. |
My main concern is not about the number of phases, but how do we anchor them to specifics: how extenders access experimental APIs that we can change? I'm fine with as many phases as we want, as long as each one has its own purposes and differentiated mechanism. My suggestion was about anchoring the conversation in specifics to understand the lay of the land — the things we can and can't do. For example, how do we handle API changes (removing things, breaking backward-compatibility for experimental APIs, etc.)? can we even do that? Do we use globals ( |
Sorry folks, I needed to let this slip so I could prioritize my 6.7 release squad duties.
The big issue I have with that is that it would reintroduce the faux-stable APIs problem once they hit stage two are are available in WP Core. When Core used the If WooCommerce or other large plugins wants to use unstable APIs in production code, that's fine but it's on WooCommerce developers to know the risk if they are working around the intentional difficulty to use them. I am happy to use a static string in the Gutenberg plugin but, yes, the requirement would be for plugins using these APIs to do version comparisons, try...catch or a wrapper component to maintain compatibility. For the GB plugin static string, I suggest something along the lines of: My strong recommendations for plugins such as WooCommerce wishing to use these APIs is to only do so in experimental features (such as the Woo block editor) that users opt-in to. If a user is opting in to bleeding edge Woo, then I think it's fair to expect them to opt-in to bleeding edge WP too. |
Thanks everyone for participating in this discussion! Let me propose a roadmap how we could move forward. There is very little new about the roadmap, it's been proposed before in #47785 and documented in the coding guidelines. It has just never been fully implemented and somewhat forgotten about. Private APIs: Gutenberg packages expose private APIs using the In #66558 I compiled a list of all 200+ private APIs, so that we can have a picture what the API surface looks like, and what is really used to build the real Gutenberg editor apps and core blocks. Experimental APIs: What plugins really want are not private APIs, but experimental APIs. Every experimental API has a clear goal to become stable and public sooner or later, and we want plugins to test them out and participate in their development. And plugins want to use them in order to start developing future features today. The experimental API would be exposed as regular package exports, without a There can be one "object" export, like: import { experimentalApis } from '@wordpress/components';
// transpiled to `const { experimentalApis } = wp.components`
const { Menu } = experimentalApis; Or we can resurrect the good old import { __experimentalMenu as Menu } from '@wordpress/components'; Nothing about this is really new, @adamziel proposed this a long time ago in #47785 and @youknowriad recently mentioned in this discussion (#66197 (comment)) that we already use What this means that a plugin can use the experimental APIs only when the Gutenberg plugin is installed. The APIs should be used only for experimental opt-in features, and the plugin should fail gracefully when Gutenberg plugin is not present. Exactly what @peterwilsoncc wrote earlier:
I know this is a very big limitation but I'm afraid we can't do any better. If something is in Core, it needs to follow the backward-compatibility policy and the experimental APIs don't satisfy that. The policy is here to protect WordPress users and their sites, and also to protect WordPress' reputation for supporting seamless automated no-brainer upgrades. If you have a site that uses a plugin, you don't really know if it internally attempts to use experimental APIs. That's a knowledge completely unaccessible to a regular user. But we want you to trust us that when you upgrade from WordPress 6.7 to 6.8, done on the day of the release, we did everything possible to not break your site. Stages: Only the experimental APIs will have stages. Private APIs are not exposed externally, and most of them are stable and not supposed to be public. Assigning them to a stage would mean that they will progress through the stages and will eventually become public. But that's not true and it could create an unwanted incentive to be used by extenders ("it's going to be stable eventually anyway so I'm actually helping you test it when I use it!") To really assign APIs to stages, we'll need to work "in the trenches" with individual API endpoints. Do we want to make it initially "private" and use it only in Gutenberg, and only later change to "experimental". Do we want to expose it to plugins from the beginning? Do we have enough use cases for it in Gutenberg itself, or do we need plugins to really validate it? |
The current Gutenberg policy, aligned with WordPress backward compatibility policy, is to not expose any private APIs to plugins at all. When a Gutenberg package exposes a private API, it's supposed to be used only by another Gutenberg package. For example, the Site Editor can use a private API from Block Editor or Components.
The packages try very hard to prevent plugins from using private API, there is the
lock
/unlock
API and also @peterwilsoncc is hardening it by regularly changing the consent string in PRs like #55182.However, this is a situation that treats the unstable APIs as "static" -- they already are here, and sometimes we review them and promote them to stable APIs. But I think we're not paying enough attention to the "dynamic" aspect, how new APIs are created and evolved before they are stabilized.
Currently, the only way how a new API can be reasonably created and evolved is when some Gutenberg app needs it. Because the Gutenberg packages can be broadly divided into "libraries" and "apps": libraries provide components, frameworks and utilities, and the apps (Post Editor, Site Editor, the 100+ core blocks) use them. If one of the apps needs something new from the libraries, we create a private API for that. That API can be modified as we learn more about the use cases and add new ones. Sometimes we stabilize an API and make it public, available to plugins.
The downside of this is that a plugin author never has a chance to participate in the development! They can use only something that is already finished. There's no way how a plugin author can experiment with private APIs and provide feedback, and participate in their evolution. Only the Gutenberg apps and blocks can be part of this process of trial and error.
This is now becoming a problem as we're working on new projects that go beyond Gutenberg, trying to rethink and refresh the entire wp-admin experience. The WooCommerce project would like to be part of the team, to use the new components like
DataView
orDataForm
and rebuild the admin experience with them. Their use cases are more complex and rich than what is in Core and Gutenberg, we need them to verify that we're doing the Core APIs right.So, can we find a way to allow plugins to participate in the evolution of new APIs, while at the same time keeping the WordPress backward compatibility contract?
One source of inspiration could be the JavaScript language and the TC39 process. After a new feature is added to the language standard, it needs to be finished and it's going to be there forever. But how is the feature developed and evolved?
Their answer is that potential new APIs go through several stages, stage 0 to stage 4. The stage signals how stable the feature is and how much you can rely on it. Developers can enable the unstable features in their build tools (the Babel transpiler etc.) or use polyfills. Browsers implement unstable APIs behind feature flags, and expose them unflagged in development versions (Google Canary, Firefox Nightly). That provides opportunities for testing, for developers to try them out and provide feedback.
Could WordPress have something similar?
The text was updated successfully, but these errors were encountered: