# Creating a Website Chat Widget with Gradio Part IV

**URL:** </static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Fcreating-a-website-chat-widget-with-gradio-part-iv%2F177208%3E%5C
**Category:** Beginners\
**Created:** [June 28, 2026, 10:16am UTC](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Fcreating-a-website-chat-widget-with-gradio-part-iv%2F177208 "2026-06-28T10:16:50Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![T40FaceHugger](https://avatars.discourse-cdn.com/v4/letter/t/71c47a/32.png) [@T40FaceHugger](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Fu%2FT40FaceHugger)%5C
**Post date:** [June 28, 2026, 10:16am UTC](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Fcreating-a-website-chat-widget-with-gradio-part-iv%2F177208%2F1 "2026-06-28T10:16:50Z")

</div>

A while back @John6666 help set up an AI gradio chatbot on my website. Everything was working fine, with the AI chatbot doing a great job.

However, I’ve not been monitoring it, and sometime recently(?) it stopped working. The problem is on the client I get the error:

> Access to fetch at [huggingfacespace] from origin [mywebsite] has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: The value of the ‘Access-Control-Allow-Credentials’ header in the response is ‘’ which must be ‘true’ when the request’s credentials mode is ‘include’.

After looking around this seems to be related to a change to gradio where they fixed a security issue with CORS. However, the solution I found appeared to suggest the fix was to basically re-write the code to “get around” CORS rather than using it.

It that really the only solution?

This is the code that was working before the “fix” broke it:

(website client code)

```auto
<script type="module">
    import { Client } from "https://cdn.jsdelivr.net/npm/@gradio/client@/dist/index.min.js";
    import { marked } from "https://cdn.jsdelivr.net/npm/marked/lib/marked.esm.js";
    
    async function initChatWidget() {
        const client = await Client.connect("https://[myspace].hf.space/");
        
        const conversationId = crypto.randomUUID(); // NEW FOR LOGGING
        const chatToggle = document.getElementById('chat-toggle');
        const chatContainer = document.getElementById('chat-container');
        const closeChat = document.getElementById('close-chat');
        const chatInput = document.getElementById('chat-input');
        const sendButton = document.getElementById('send-message');
        const messagesContainer = document.getElementById('chat-messages');
    
        chatToggle.addEventListener('click', () => {
            chatContainer.classList.remove('hidden');
        });
    
        closeChat.addEventListener('click', () => {
            chatContainer.classList.add('hidden');
        });
    
        // Auto-open chat on contact page
if (window.location.pathname.includes('/contact') ||
    window.location.pathname.endsWith('/contact.html') ||
    window.location.pathname.includes('/account') ||
    window.location.pathname.endsWith('/account.html')) {
    chatContainer.classList.remove('hidden');
}
    
        function appendMessage(text, sender) {
            const div = document.createElement("div");
            div.className = `message ${sender}-message`;
            div.textContent = text; // or marked.parse(text) if you're using marked
            messagesContainer.appendChild(div);
            messagesContainer.scrollTop = messagesContainer.scrollHeight;
        }
    
        async function sendMessage() {
            const userMessage = chatInput.value.trim();
            if (!userMessage) return;

            appendMessage(userMessage, 'user');
            chatInput.value = '';

            try {
                const result = await client.predict("/chat", {
                    message: {"text": userMessage, "files": []},
                conversation_id: conversationId, // NEW FOR LOGGING
                });

      const lines = result.data[0]; // list of strings from Python
      const botMessage = Array.isArray(lines) ? lines.join("\n") : String(lines);
      appendMessage(botMessage, "bot");
            
            } catch (error) {
                console.error('Error:', error);
                appendMessage('Sorry, there was an error processing your request.', 'bot');
            }
        }
 
        sendButton.addEventListener('click', sendMessage);
        chatInput.addEventListener('keypress', (e) => {
            if (e.key === 'Enter') sendMessage();
        });
    
      // PRE-SEED: show a bot greeting as soon as the widget is ready
      appendMessage("Hey there! What can I help you with today?", "bot");
      }
    
      initChatWidget();
</script>

```

(code on huggingface space)

```auto

import json
import requests # for API code
import holidays # details of UK holidays
from datetime import datetime, timedelta, date, time
from openai import OpenAI
import gradio as gr
import os
import uuid

say_hello = [
    {
        "role": "assistant",
        "content": "Hey there! What can I help you with today?",
    }
]

# This is the main chat code called by Gradio ChatInterface on user input, sends input to AI, processes response, returns to the Gradio ChatInterface
def chat(message, history, conversation_id):
    if not conversation_id:
        conversation_id = str(uuid.uuid4())

    if isinstance(message, dict): # make sure the user comment is in an acceptable format, and convert to a string
        user_text = message.get("text", "")
    else:
        user_text = str(message)
    history = [{"role":h["role"], "content":h["content"]} for h in history] # clean history for some non openai models
    messages = [{"role": "system", "content": system_message}] + history + [{"role": "user", "content": user_text}] # illusion of memory
    response = openai.chat.completions.create(model=MODEL, messages=messages, tools=tools)

    # AI STUFF HERE

    response_as_a_list = [response.choices[0].message.content] # need to change reponse from string to a list of strings for the chatbot widget
    return response_as_a_list

# Additional (hidden) input so ChatInterface exposes 'conversation_id'
conversation_id_input = gr.Textbox(
    label="conversation_id",
    visible=False,
    value="",
)

# Use the Gradio Chatbot component to preload the chat (and history) with an initial greeting, rather than letting the user go first
chatbot = gr.Chatbot(
    value=say_hello,
    type="messages",
)

# The main chat interface handled by Gradio
my_chat = gr.ChatInterface(
    fn=chat,
    type="messages",
    multimodal=True,
    title="Widget Demo Bot",
    api_name="chat",
    additional_inputs=[conversation_id_input], # extra arg to fn
)

if __name__ == " __main__": 
    my_chat.launch(ssr_mode=False)                                                 

```

---

<div class="post-metadata">

**Author:** ![John6666](https://sea2.discourse-cdn.com/hellohellohello/user_avatar/discuss.huggingface.co/john6666/32/27664_2.png) [@John6666](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Fu%2FJohn6666)%5C
**Post date:** [June 29, 2026, 7:13am UTC](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Fcreating-a-website-chat-widget-with-gradio-part-iv%2F177208%2F2 "2026-06-29T07:13:14Z")

</div>

Hi. Short version: I think this **is probably a bug** , and probably not something caused by you or your Space:

* * *

The most useful way to frame this is probably:

> Direct/canonical paths work, but the browser-facing Gradio Web Component / Space identity / host / config-resolution path fails.

So I would **not** start by changing your Python code, model code, or Gradio layout. Your checks already point away from “the Space app itself is broken” and toward something in the path used by the Web Component to resolve/load the Space.

The strongest clues are:

| Check | Result | What it suggests |
| --- | --- | --- |
| Direct Space URL | works | the Space runtime is serving |
| Direct `https://<space-subdomain>.hf.space/config` | works | the Gradio app can expose its config from its own host |
| iframe embed | works | normal browser embedding of the Space works |
| `https://huggingface.co/api/spaces/<owner>/<space_name>` | works | the canonical Space repo-id API path works |
| `<gradio-app src="https://<space-subdomain>.hf.space">` | fails | Web Component + subdomain path fails |
| `<gradio-app space="<owner>/<space_name>">` | fails | Web Component + owner/name → host/config path also fails |
| `https://huggingface.co/api/spaces/by-subdomain/<space-subdomain>` | fails | the subdomain → Space identity lookup path is suspicious |

That combination is hard to explain as a bug inside the user Space runtime.

The closest current GitHub issue I found is this one:

- [gradio-app/gradio#13554 — Web components fail to load due to error in `/api/spaces/by-subdomain/<subdomain>`](https://github.com/gradio-app/gradio/issues/13554)

I would not claim that this proves the exact same internal root cause, but the public symptom is extremely close: same `by-subdomain` endpoint, same encoded-slash style error, and the same split between `src=` and `space=` failure modes.

> **Why I think this points away from your Space app**
>
> I would separate the paths like this:
> 
> ### 1. Space runtime path
> 
> These check whether the app itself is basically alive:
> 
> ```plaintext
> https://<space-subdomain>.hf.space/
> https://<space-subdomain>.hf.space/config
> 
> ```
> 
> If those work, then the Gradio app is at least serving normally from its own `.hf.space` host.
> 
> ### 2. Normal embed path
> 
> This checks whether the Space can be embedded as a plain iframe:
> 
> ```html
> <iframe src="https://<space-subdomain>.hf.space"></iframe>
> 
> ```
> 
> If this works too, then ordinary browser embedding is probably not the problem.
> 
> ### 3. Web Component path
> 
> These go through additional Gradio Web Component / JS client logic:
> 
> ```html
> <gradio-app src="https://<space-subdomain>.hf.space"></gradio-app>
> 
> <gradio-app space="<owner>/<space_name>"></gradio-app>
> 
> ```
> 
> That path is more than “open the Space in an iframe”. It has to resolve Space metadata, status, host/config, and then load the Gradio app through the Web Component client path.
> 
> So when the first group works and the Web Component group fails, I would not treat `app.py` as the first suspect.
> 
> This is also consistent with the official docs: Hugging Face documents direct Space URLs as `https://<space-subdomain>.hf.space`, iframe embedding, and Gradio Web Component embedding as related but separate embed modes. See [HF Spaces embed docs](https://huggingface.co/docs/hub/spaces-embed). Gradio also documents both `src` and `space` attributes for hosted Spaces in [Sharing Your App / Embedding Hosted Spaces](https://www.gradio.app/guides/sharing-your-app).

> **Closest issue: gradio-app/gradio#13554**
>
> This issue looks like the closest match:
> 
> - [gradio-app/gradio#13554](https://github.com/gradio-app/gradio/issues/13554)
> 
> The issue says the embedded Gradio app started failing around June 22, while direct Space access still worked. The reported failing request is:
> 
> ```plaintext
> https://huggingface.co/api/spaces/by-subdomain/<space-subdomain>
> 
> ```
> 
> with an error like:
> 
> ```json
> {
> "error": "Invalid repo name: <owner>/<space> - repo name includes an url-encoded slash"
> }
> 
> ```
> 
> It also reports two different failure modes:
> 
> ```html
> <gradio-app src="https://<space-subdomain>.hf.space"></gradio-app>
> 
> ```
> 
> fails through the `by-subdomain` path, while:
> 
> ```html
> <gradio-app space="<owner>/<space>"></gradio-app>
> 
> ```
> 
> fails around the config/CORS/retry path.
> 
> The issue also says this reproduced with Gradio `5.49.1` and with a new test app using `6.19.0`, and in both Chrome and Firefox. So I would not reduce this to “just update Gradio” unless a maintainer has since confirmed a fixed release.
> 
> There is also an older issue with the same endpoint/error family:
> 
> - [gradio-app/gradio#12570 — 404 Bad Request using Hugging Face Spaces API](https://github.com/gradio-app/gradio/issues/12570)
> 
> I would treat #12570 as useful background for the `by-subdomain` / encoded-slash error pattern, not as proof that the June 22 behavior has the same cause.

> **Why \`src=\` and \`space=\` may be failing differently**
>
> I would separate the two failing paths:
> 
> ### A. `src="https://<space-subdomain>.hf.space"`
> 
> This starts from the `.hf.space` subdomain form:
> 
> ```html
> <gradio-app src="https://<space-subdomain>.hf.space"></gradio-app>
> 
> ```
> 
> The suspicious failing request is:
> 
> ```plaintext
> https://huggingface.co/api/spaces/by-subdomain/<space-subdomain>
> 
> ```
> 
> So this points toward the subdomain → Space identity/status lookup path.
> 
> ### B. `space="<owner>/<space_name>"`
> 
> This starts from the canonical Space repo id form:
> 
> ```html
> <gradio-app space="<owner>/<space_name>"></gradio-app>
> 
> ```
> 
> In your test, this did not avoid the problem. Instead, the component repeatedly tried to fetch something like:
> 
> ```plaintext
> https://.hf.space/config
> 
> ```
> 
> A literal empty-host URL like that is especially suspicious. It suggests that some host/config resolution step failed before the request could even meaningfully reach your Space app.
> 
> So I would describe the common layer as something like:
> 
> ```plaintext
> browser-facing Space identity / host / config resolution used by the Gradio Web Component
> 
> ```
> 
> That is different from:
> 
> ```plaintext
> the Python function inside the Space crashed
> 
> ```
> 
> or:
> 
> ```plaintext
> the Gradio Blocks/Interface definition is invalid
> 
> ```
> 
> A code-level detail that supports this general separation: the Gradio JS client has separate handling for Space names, Space domains, Space status checks, and `/config` resolution. For example, current source has a `check_space_status()` helper that chooses between:
> 
> ```plaintext
> https://huggingface.co/api/spaces/by-subdomain/<id>
> https://huggingface.co/api/spaces/<id>
> 
> ```
> 
> depending on whether the identifier is treated as a subdomain or a Space name. See:
> 
> - [`client/js/src/helpers/spaces.ts`](https://raw.githubusercontent.com/gradio-app/gradio/main/client/js/src/helpers/spaces.ts)
> - [`client/js/src/helpers/api_info.ts`](https://raw.githubusercontent.com/gradio-app/gradio/main/client/js/src/helpers/api_info.ts)
> - [`client/js/src/helpers/init_helpers.ts`](https://raw.githubusercontent.com/gradio-app/gradio/main/client/js/src/helpers/init_helpers.ts)
> 
> I would still be careful here: this does not prove the internal HF-side root cause. But it does explain why Web Component loading can fail at a different layer than direct `.hf.space` or iframe loading.

## Related, but I would not merge them as the same bug

The chat-widget/CORS thread you linked also looks relevant as context:

- [Creating a Website Chat Widget with Gradio Part IV](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Fcreating-a-website-chat-widget-with-gradio-part-iv%2F177208)

There is also this nearby thread:

- [Hugging Face Spaces proxy suddenly stripping `Access-Control-Allow-Credentials` header on OPTIONS preflight?](/static-proxy?url=https%3A%2F%2Fdiscuss.huggingface.co%2Ft%2Fhugging-face-spaces-proxy-suddenly-stripping-access-control-allow-credentials-header-on-options-preflight%2F177064)

I would **not** say these prove the same root cause. They are different surface symptoms:

| Thread | Surface symptom |
| --- | --- |
| this thread | Web Component fails; `by-subdomain` returns bad repo-name / encoded-slash error; `space=` can lead into bad config/CORS/retry behavior |
| chat-widget thread | `@gradio/client` call from an external site fails around credentialed CORS |
| Spaces proxy thread | `OPTIONS` preflight appears to miss `Access-Control-Allow-Credentials` |

But they are useful context because the diagnostic shape is similar:

> the app/container may be healthy, while an external browser-facing integration path fails before useful app logic runs.

For CORS specifically, MDN’s general rule is that credentialed cross-origin requests need `Access-Control-Allow-Credentials: true` in the relevant server response. See [MDN: Access-Control-Allow-Credentials](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Access-Control-Allow-Credentials) and [MDN: Reason: expected `true` in CORS header `Access-Control-Allow-Credentials`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS/Errors/CORSMIssingAllowCredentials).

That matters for the chat-widget case, but I would keep it secondary here. In this thread, the cleanest signal is still the `by-subdomain` / Web Component / host-config-resolution failure.

## What I would attach to the GitHub issue or to a follow-up here

The most helpful next step is probably not “try random fixes”, but to make the reproduction mechanically clear and attach it to [gradio-app/gradio#13554](https://github.com/gradio-app/gradio/issues/13554), or at least cross-link this forum thread from there.

Something like this would be very useful:

```plaintext
I may be seeing the same Web Component / Space resolution issue.

Space repo:
<owner>/<space_name>

Direct Space URL:
https://<space-subdomain>.hf.space/

Space visibility:
public / protected / private

Gradio SDK version in the Space:
...

Web Component JS version:
...

Browser / OS:
...

Direct checks:
- https://<space-subdomain>.hf.space/ => works / fails
- https://<space-subdomain>.hf.space/config => works / fails
- https://huggingface.co/api/spaces/<owner>/<space_name> => works / fails
- https://huggingface.co/api/spaces/by-subdomain/<space-subdomain> => works / fails

Embed checks:
- iframe => works / fails
- <gradio-app src="https://<space-subdomain>.hf.space"></gradio-app> => works / fails
- <gradio-app space="<owner>/<space_name>"></gradio-app> => works / fails

First failing request in DevTools Network:
...

HTTP status:
...

Response body:
...

Important response headers, if present:
- x-request-id:
- x-error-message:
- x-cache:
- via:
- cloudfront / cf / cache-related headers:

Notes:
- whether it started around June 22
- whether it reproduces with a brand-new minimal Space
- whether it reproduces in another browser
- whether the same page works with iframe

```

The key thing is to show the split:

```plaintext
direct/canonical paths work
derived/browser-facing Web Component paths fail

```

That is the part that makes this look platform/component-side rather than app-code-side.

> **Small test matrix I would use**
>
> ```bash
> # Direct app/runtime check
> curl -i "https://<space-subdomain>.hf.space/"
> 
> # Direct Gradio config check
> curl -i "https://<space-subdomain>.hf.space/config"
> 
> # Canonical Space API check
> curl -i "https://huggingface.co/api/spaces/<owner>/<space_name>"
> 
> # Subdomain lookup check
> curl -i "https://huggingface.co/api/spaces/by-subdomain/<space-subdomain>"
> 
> ```
> 
> Then compare browser behavior using the same Space:
> 
> ```html
> <!-- iframe: normal embed path -->
> <iframe
> src="https://<space-subdomain>.hf.space"
> width="850"
> height="450">
> </iframe>
> 
> <!-- Web Component, direct Space URL form -->
> <gradio-app src="https://<space-subdomain>.hf.space"></gradio-app>
> 
> <!-- Web Component, canonical Space repo-id shorthand -->
> <gradio-app space="<owner>/<space_name>"></gradio-app>
> 
> ```
> 
> Expected diagnostic interpretation:
> 
> | Pattern | Likely direction |
> | --- | --- |
> | Direct Space fails | app/runtime/startup problem is plausible |
> | Direct Space works, iframe works, Web Component fails | Web Component / JS client / config / metadata path is more likely |
> | Canonical API works, `by-subdomain` fails | subdomain lookup path is suspicious |
> | `space=` generates `https://.hf.space/config` | resolved host/config path is suspicious |
> | only credentialed browser requests fail | CORS/proxy/header behavior becomes more relevant |
> | only private/protected/auth Spaces fail | visibility/auth/cookie handling becomes more relevant |

## What I would _not_ try first

I would not start with:

- rewriting the Gradio app
- changing the model code
- changing the Blocks/Interface structure
- treating this as an app startup problem
- broadly weakening CORS
- assuming `strict_cors=False` is the fix

`strict_cors=False` can be relevant in a different situation, especially local/null-origin Web Component testing. Gradio’s docs describe `strict_cors` as a localhost/null-origin protection knob, and the Gradio 5 migration note mentions local-file Web Component testing as the case where disabling it may be needed. See:

- [Gradio Blocks docs: `strict_cors`](https://www.gradio.app/docs/gradio/blocks)
- [Migrating to Gradio 5 — CORS / `strict_cors` note](https://github.com/gradio-app/gradio/issues/9463)
- [Gradio security advisory: CORS origin validation accepts the null origin](https://github.com/gradio-app/gradio/security/advisories/GHSA-89v2-pqfv-c5r9)

That does not look like the first fix for this specific report, because this is a hosted HF Space where direct access, direct `/config`, and iframe have already been reported as working.

## Temporary workaround

If this is blocking a real page, I would use iframe temporarily.

That is not a root fix for the Web Component bug, but iframe embedding is an official HF Space embed path, and it avoids some of the extra Web Component-specific Space lookup/config behavior.

```html
<iframe
  src="https://<space-subdomain>.hf.space"
  width="850"
  height="450"
  style="border:0;">
</iframe>

```

Then, once the Web Component issue is fixed, it should be reasonable to switch back to the documented Web Component form:

```html
<script
  type="module"
  src="https://gradio.s3-us-west-2.amazonaws.com/<GRADIO_VERSION>/gradio.js">
</script>

<gradio-app src="https://<space-subdomain>.hf.space"></gradio-app>

```

or:

```html
<script
  type="module"
  src="https://gradio.s3-us-west-2.amazonaws.com/<GRADIO_VERSION>/gradio.js">
</script>

<gradio-app space="<owner>/<space_name>"></gradio-app>

```

## Short version

I would treat this as a likely Web Component / HF Space identity-resolution / host/config-resolution bug until proven otherwise.

The strongest next action is probably to add your reproduction matrix to:

- [gradio-app/gradio#13554](https://github.com/gradio-app/gradio/issues/13554)

especially because your tests already show the important split:

```plaintext
works:
- direct Space
- direct /config
- iframe
- canonical /api/spaces/<owner>/<space_name>

fails:
- Web Component src=
- Web Component space=
- /api/spaces/by-subdomain/<space-subdomain>

```

That is the useful signal for maintainers and for anyone else trying to debug the same failure later.
