[← Back to Reviews Index](../Stewards%20Reviews%20Index.md)

# React API Client Review — Azure-AI-RAG-CSharp-Semantic-Kernel-Functions

| Field | Value |
|---|---|
| **Project** | Azure-AI-RAG-CSharp-Semantic-Kernel-Functions |
| **Steward** | React API Client Steward (RAPI) |
| **Run date** | 2026-03-21 |
| **Severity summary** | 🔴 Critical: 3 · 🟡 Notable: 6 · 🟢 Minor: 2 · ℹ️ Info: 2 |
| **Total findings** | 13 |

---

## 1. API Client Architecture Overview

The React frontend (`src/web/`) is a Create React App (CRA) project using React 18, JavaScript (no TypeScript), and Material UI. It consists of a single meaningful interactive component — `SupportAgent/Agent.js` — which directly issues two `fetch()` calls to the backend C# API:

- `GET /session` — acquires a session ID on component mount.
- `POST /chat` — submits user prompts and receives AI responses.

There is no centralized API client module, no HTTP abstraction layer, no authentication layer, no request cancellation, no loading state management, no error handling, and no TypeScript typing. The entire API integration surface is a single 90-line JavaScript file with bare `fetch` calls chained by `.then()` with no `.catch()`.

The project is explicitly marked as a demo/educational artifact in its README and includes a disclaimer in the UI. Nevertheless, findings are reported against the code as written, as the steward audits patterns rather than deployment intent.

---

## 2. HTTP Client Abstraction Assessment

There is no centralized API client module. Both `fetch()` calls are inlined directly inside the `SupportAgent` component event handler (`handlePrompt`) and a lifecycle handler (`handleSession`). Each call manually sets headers, constructs URLs by concatenating `process.env.REACT_APP_API_HOST` with a path suffix, and manages its own `.then()` chain independently.

No `axios` instance, `fetch` wrapper, React Query, SWR, or equivalent abstraction exists. The `dotenv` package is listed as a dependency, but base URL configuration is handled via Create React App's native `REACT_APP_*` environment variable mechanism, which is appropriate and correct.

**Base URL:** `process.env.REACT_APP_API_HOST` — this is correctly externalized via environment variable. The `README.md` documents that developers must create a `.env` file with `REACT_APP_API_HOST=http://127.0.0.1:8000`. The `.gitignore` correctly excludes `.env` from version control. This is the one area of the API integration that is correctly implemented.

**Common headers:** `Content-Type: application/json` is set on the POST request only. No shared header injection pattern exists — if additional requests were added, each would need to re-declare headers individually.

---

## 3. Authentication Handling Assessment

There is no authentication in the frontend. No `Authorization` header is sent on any request. No token storage, token refresh, or session management exists beyond the session ID (`sessionid`) passed as a JSON body field in the `/chat` request. This session ID is obtained from the backend's `/session` endpoint and held in component state.

Given the project is a demo, this may be intentional. However, it is flagged because the absence of any auth leaves the `/chat` endpoint open to any caller who can reach the backend.

No auth tokens are stored in `localStorage` (no such code exists), so that specific risk category is not applicable here.

---

## 4. Error Handling Assessment

This is the most severe area of concern.

### `handlePrompt` (POST /chat)

```js
fetch(process.env.REACT_APP_API_HOST + '/chat', { ... })
  .then((response) => response.json())
  .then((res) => {
    console.log(res);
    setMessage(...)
  })
```

There is no `.catch()`. If the network request fails, `response.json()` throws (e.g., on a non-200 response with a non-JSON body), or the server returns an error JSON, the promise rejection is silently swallowed by the runtime. The user receives no feedback. The UI remains in an indeterminate state with the user's prompt appended but no reply ever arriving.

Additionally, `response.ok` is never checked. A `400` or `500` response from the server will be passed through `.json()` just like a success, meaning HTTP error codes are invisible to the application.

### `handleSession` (GET /session)

```js
fetch(process.env.REACT_APP_API_HOST + '/session')
  .then((response) => response.json())
  .then((res) => {
    setSession(res.session_id)
  })
```

Same pattern — no `.catch()`, no `response.ok` check. If the session endpoint fails on mount, `session` remains `''` and every subsequent chat request will be sent with `sessionid: ''`, potentially causing server-side errors with no indication to the user.

### `console.log(res)` in production

`console.log(res)` is called on every chat API response. In production builds this leaks API response payloads (potentially including AI-generated content, session identifiers, or internal metadata) to the browser console. While not as severe as logging auth tokens, it is an unintended data exposure vector.

---

## 5. Loading / Request State Assessment

There is no loading state. When a user clicks "Submit":

1. The prompt is appended to the message list immediately.
2. The fetch is fired.
3. There is no spinner, disabled state on the "Submit" button, or any indicator that a request is in-flight.

The "Submit" button remains active throughout the request lifecycle. This means a user can click "Submit" multiple times while a request is pending, firing multiple concurrent `POST /chat` requests. Because responses are appended to the message list in arrival order, responses can appear out of order relative to the prompts that generated them.

No React Query, SWR, or equivalent data-fetching library is present.

---

## 6. Race Condition Assessment

Two race condition vectors exist:

### Multiple concurrent submits

The Submit button is never disabled during a pending request. Rapid clicking submits multiple concurrent POST requests. Since all `.then()` callbacks call `setMessage((message) => [...message, ...])`, responses arrive and append in network-arrival order, not submission order. There is no request queue or serialization.

### Unmount without cancellation

The `useEffect` that triggers `handleSession()` on mount has no cleanup function and no `AbortController`. If the component unmounts before the `/session` response arrives, the `.then()` callback will call `setSession(...)` on an unmounted component. In React 18 this does not throw (the strict-mode double-invoke behavior aside), but it is a state-update-on-unmounted-component pattern that is considered a defect.

```js
useEffect(() => {
  if (session === '') handleSession()
}, [])
```

No `AbortController` is used anywhere in the codebase. Neither the session fetch nor the chat fetch can be cancelled.

### Missing `session` dependency in `useEffect`

The `useEffect` dependency array is empty (`[]`), but the effect checks `session === ''`. While functionally harmless here (the session is fetched exactly once on mount), the `session` variable is read inside the effect without being declared as a dependency, which would trigger an ESLint `react-hooks/exhaustive-deps` warning and is a latent correctness risk if the component's lifecycle changes.

---

## 7. TypeScript Typing Assessment

The project is plain JavaScript — there are no TypeScript files, no `tsconfig.json`, and `typescript` is not listed as a dependency. All API response shapes are untyped.

- The `/session` response is consumed as `res.session_id` with no type guard or shape validation.
- The `/chat` response is consumed as `res.resp` with no type guard or shape validation.
- If either response shape changes server-side, the failure is silent at runtime (`undefined` values propagate into state and render).

No `PropTypes` are defined on any component. `ChatLayout.js` accesses `messages.messages` (a prop it receives), with no validation that the prop exists or has the expected shape.

---

## 8. Streaming Assessment

The `/chat` call uses a standard `fetch` + `response.json()` pattern — it waits for the complete response body before rendering. There is no Server-Sent Events (SSE) client, no `ReadableStream` consumption, and no WebSocket connection. The backend C# API (`ChatAPI`) may return full JSON responses synchronously, so this may be architecturally consistent.

No streaming-specific issues to report. If streaming is added in the future, cancellation and chunk-handling will need to be built from scratch as no infrastructure exists.

---

## 9. Findings

| Severity | ID | Title | File |
|---|---|---|---|
| 🔴 Critical | RAPI-ERROR-001 | No error handling on `/chat` POST — unhandled promise rejection | `src/SupportAgent/Agent.js` |
| 🔴 Critical | RAPI-ERROR-002 | No error handling on `/session` GET — silent session failure | `src/SupportAgent/Agent.js` |
| 🔴 Critical | RAPI-ERROR-003 | HTTP error status codes never checked (`response.ok` not tested) | `src/SupportAgent/Agent.js` |
| 🟡 Notable | RAPI-ABSTRACT-001 | No centralized API client — raw `fetch` calls inlined in component | `src/SupportAgent/Agent.js` |
| 🟡 Notable | RAPI-LOADING-001 | No loading state — Submit button not disabled during in-flight request | `src/SupportAgent/Agent.js` |
| 🟡 Notable | RAPI-RACE-001 | Multiple concurrent submits possible — no request serialization or deduplication | `src/SupportAgent/Agent.js` |
| 🟡 Notable | RAPI-RACE-002 | No `AbortController` on `/session` fetch — state update on unmounted component | `src/SupportAgent/Agent.js` |
| 🟡 Notable | RAPI-TYPE-001 | No TypeScript — API response shapes entirely untyped | `src/SupportAgent/Agent.js` |
| 🟡 Notable | RAPI-TYPE-002 | No PropTypes on `ChatLayout` — missing shape validation for `messages` prop | `src/SupportAgent/ChatLayout.js` |
| 🟢 Minor | RAPI-ERROR-004 | `console.log(res)` leaks API response to browser console in production | `src/SupportAgent/Agent.js` |
| 🟢 Minor | RAPI-RACE-003 | Missing `session` in `useEffect` dependency array | `src/SupportAgent/Agent.js` |
| ℹ️ Info | RAPI-ABSTRACT-002 | Base URL correctly externalized via `REACT_APP_API_HOST` env variable | `src/SupportAgent/Agent.js` |
| ℹ️ Info | RAPI-ABSTRACT-003 | `.env` correctly excluded from version control via `.gitignore` | `.gitignore` |

### Finding Details

#### RAPI-ERROR-001 — No error handling on `/chat` POST (Critical)

The `handlePrompt` function chains two `.then()` calls on the `/chat` fetch but has no `.catch()`. Any network failure, DNS error, CORS rejection, or JSON parse error causes an unhandled promise rejection. The user's prompt is appended to the chat history but no reply ever arrives — the UI hangs silently with no feedback.

**Recommendation:** Add `.catch(err => { /* set error state, show error message */ })` and check `response.ok` before calling `response.json()`.

#### RAPI-ERROR-002 — No error handling on `/session` GET (Critical)

The `handleSession` function has no `.catch()`. If the session endpoint is unavailable on mount (network down, backend cold start, CORS), `session` stays as `''`. Every subsequent `/chat` POST will send `sessionid: ''`. The user has no indication anything went wrong, and the backend may fail silently on empty session IDs.

**Recommendation:** Add `.catch()` to `handleSession`, surface the error state, and prevent the Submit button from being active until a valid session is established.

#### RAPI-ERROR-003 — HTTP error codes never checked (Critical)

Neither fetch call inspects `response.ok` before calling `response.json()`. A `400 Bad Request`, `500 Internal Server Error`, or `503 Service Unavailable` from the backend will be treated as a successful response, and `response.json()` will attempt to parse the error body. If the error body is not valid JSON, this throws and falls into the unhandled rejection. If it is valid JSON, the error payload will be passed to `setMessage` with `res.resp` being `undefined`, rendering a blank chat bubble.

**Recommendation:**

```js
fetch(url, options)
  .then(response => {
    if (!response.ok) throw new Error(`API error: ${response.status}`);
    return response.json();
  })
  .then(...)
  .catch(err => setError(err.message));
```

#### RAPI-ABSTRACT-001 — No centralized API client (Notable)

Both API calls are raw `fetch` invocations inlined inside component handlers. As the application grows, each new API call will duplicate URL construction, header setup, and `.catch()` boilerplate. There is no single place to add cross-cutting concerns (auth headers, request logging, retry logic).

**Recommendation:** Extract a minimal `apiClient.js` module that wraps `fetch` with base URL injection, default headers, `response.ok` checking, and JSON parsing. Components call `apiClient.post('/chat', body)` rather than raw `fetch`.

#### RAPI-LOADING-001 — No loading state (Notable)

The Submit button has no disabled state and there is no loading indicator. Users can submit multiple times while a request is pending. Additionally, there is no visual confirmation that the request is in-flight, which degrades perceived responsiveness for a chat application where responses may take several seconds (LLM inference latency).

**Recommendation:** Add a `loading` state variable, disable the Submit button while `loading === true`, and display a typing indicator in the chat area.

#### RAPI-RACE-001 — Multiple concurrent submits (Notable)

Because the Submit button is never disabled, rapid clicks fire multiple concurrent POST requests. Responses are appended to the message list in network-arrival order, not prompt order. For an AI chat application, out-of-order responses can be confusing and misleading.

**Recommendation:** Disable the Submit button while a request is pending (RAPI-LOADING-001 fix resolves this) and/or use a request queue.

#### RAPI-RACE-002 — No `AbortController` on session fetch (Notable)

The `useEffect` on mount fires `handleSession()` with no cleanup return. If the component unmounts before the fetch resolves, `setSession(res.session_id)` is called on an unmounted component. In React 18 StrictMode, effects run twice in development, meaning two concurrent session fetches may race.

**Recommendation:**

```js
useEffect(() => {
  const controller = new AbortController();
  handleSession(controller.signal);
  return () => controller.abort();
}, []);
```

Pass `signal` to `fetch()` options.

#### RAPI-TYPE-001 — No TypeScript, API responses untyped (Notable)

The project uses plain JavaScript. `res.session_id` and `res.resp` are accessed without any runtime validation. If the backend changes response field names, the failure is silent (`undefined` renders as nothing in the chat bubble). Migrating to TypeScript and defining response interfaces would catch these mismatches at build time.

**Recommendation:** Migrate to TypeScript and define interfaces for API response types:

```ts
interface SessionResponse { session_id: string; }
interface ChatResponse { resp: string; }
```

At minimum, add runtime shape guards if TypeScript migration is not feasible.

#### RAPI-TYPE-002 — No PropTypes on `ChatLayout` (Notable)

`ChatLayout.js` accesses `messages.messages` with no `PropTypes` definition. If `Agent.js` ever passes incorrectly shaped props, the failure manifests as a runtime crash in the map call.

**Recommendation:** Add PropTypes validation or migrate to TypeScript with typed props.

#### RAPI-ERROR-004 — `console.log` leaks API responses in production (Minor)

Line 38 of `Agent.js`: `console.log(res)` logs the full `/chat` API response to the browser console on every request. In production builds, this exposes AI response payloads, session IDs embedded in the response, and any server-side metadata to anyone with DevTools access.

**Recommendation:** Remove the `console.log` or guard it behind a development-only check: `if (process.env.NODE_ENV === 'development') console.log(res);`

#### RAPI-RACE-003 — Missing `session` in `useEffect` dependency array (Minor)

The `useEffect` reads `session` to decide whether to call `handleSession`, but `session` is not in the dependency array. While harmless with the current single-mount behavior, ESLint `react-hooks/exhaustive-deps` would flag this, and it creates a maintenance trap if the component's lifecycle is refactored.

**Recommendation:** Either add `session` to the dependency array (and handle the re-fetch logic explicitly) or restructure so the condition is outside the effect.

---

## 10. Recommended Improvements

| Finding | Recommended action | Priority |
|---|---|---|
| RAPI-ERROR-001 | Add `.catch()` to `/chat` fetch and surface error to user | High |
| RAPI-ERROR-002 | Add `.catch()` to `/session` fetch; prevent Submit until session valid | High |
| RAPI-ERROR-003 | Check `response.ok` before `.json()` in both fetch calls | High |
| RAPI-ABSTRACT-001 | Extract `apiClient.js` wrapper with base URL, headers, error handling | Medium |
| RAPI-LOADING-001 | Add `loading` state; disable Submit button while request is in-flight | Medium |
| RAPI-RACE-001 | Resolved by RAPI-LOADING-001 fix (disabled button prevents concurrent submits) | Medium |
| RAPI-RACE-002 | Add `AbortController` to session `useEffect` cleanup | Medium |
| RAPI-TYPE-001 | Migrate to TypeScript; define `SessionResponse` and `ChatResponse` interfaces | Medium |
| RAPI-TYPE-002 | Add PropTypes to `ChatLayout` or migrate to TypeScript with typed props | Low |
| RAPI-ERROR-004 | Remove or dev-guard `console.log(res)` | Low |
| RAPI-RACE-003 | Fix `useEffect` dependency array to include `session` | Low |

---

## Cross-Cutting Observations

- The project is a demo application and is marked as such prominently in the README and in the UI itself. Many of the findings above would be low-risk in a controlled demo environment where the backend is trusted and only one user interacts with it at a time. However, the patterns are reproduced verbatim in production forks of demo code, so they are reported at their full severity.
- No authentication is implemented. This is noted here as an observation; the React Auth Steward (RAUTH) owns auth flow findings.
- The `html-react-parser` dependency parses AI-generated HTML responses directly into the DOM (`ChatLayout.js` line 13: `parse(obj.message)`). This is an XSS vector if the backend ever returns unsanitized content. This finding is in scope for the React UX Steward (RUX) and Security Steward (SEC), not this steward.

---

## Footer

This review is based on static analysis of source files only. No runtime behavior, network traffic, or build output was observed. Findings reflect code patterns at the time of analysis (2026-03-21) and may not reflect all runtime conditions.

**Steward:** React API Client Steward (`react-api-client-steward`) | **PREFIX:** RAPI
