# Analysis: 401 Only on PUT /api/companies/{id} (Other Endpoints Work)

**Scope:** Analysis only — no code changes.  
**Goal:** Explain why **only** PUT /api/companies/{id} returns 401 while other authenticated endpoints (e.g. GET /companies/{id}) succeed.

---

## STEP 1 — Verify request headers (trace PUT for editCompany)

### 1.1 Does this request use the shared Http instance?

**Yes.**

- `CompanyRepository` (companyRepository.ts) has `private readonly http: Http = Http.getInstance();`.
- `editCompany()` calls `this.http.put<Company>(`${COMPANY_BASE}/${id}`, company)`.
- So the PUT uses the **same singleton** `Http.getInstance()` as every other method in `CompanyRepository` (getCompanyById, deleteCompany, postNewCompany, etc.).

### 1.2 Does it go through HttpImplementation.put()?

**Yes.**

- `Http` (Http.ts) is a wrapper; its `put<T>()` delegates to `this.httpInmplementation.put(url, body, responseType)`.
- The implementation is `HttpImplementation` (HttpImplementation.ts).
- So the call chain is: `CompanyRepository.editCompany` → `Http.put` → `HttpImplementation.put`.

### 1.3 Does buildRequestHeaders() get called?

**Yes.**

- In `HttpImplementation.put()` (lines 94–115), the first line inside the method is:
  - `const headers = await this.buildRequestHeaders();`
- Then, for FormData, Content-Type is **not** set (so the browser can set `multipart/form-data` with boundary).
- The same `headers` object (which includes `Accept` and, if a token exists, `Authorization: Bearer <token>`) is passed to `axios.put(url, body, { headers, ... })`.
- So for every PUT, **buildRequestHeaders() is called** and the returned headers (including Authorization when the cookie has a token) are sent with the request.

**Conclusion:** The PUT request uses the shared Http instance, goes through `HttpImplementation.put()`, and **does** call `buildRequestHeaders()`. There is no code path that skips auth headers for this call.

---

## STEP 2 — FormData behavior (can it affect headers?)

### 2.1 Are headers overridden anywhere?

**No.**

- Headers are built once: `const headers = await this.buildRequestHeaders();`.
- For FormData, the only conditional is: `if (!(body instanceof FormData)) { headers["Content-Type"] = "application/json"; }`. So with FormData, **no** Content-Type is set; the object is not replaced.
- The same `headers` reference is passed to `axios.put(url, body, { headers, ... })`. Axios uses these as the request headers and, for FormData in the browser, typically **adds** `Content-Type: multipart/form-data; boundary=...` (or leaves it to the browser). It does **not** replace or strip other headers like `Authorization`.

### 2.2 Is Content-Type manually set?

**No for FormData.**

- The code explicitly **avoids** setting Content-Type when the body is FormData, so the browser/axios can set the multipart boundary. So Content-Type is not manually set for this PUT.

### 2.3 Could the Authorization header be lost?

**Not by the frontend code.**

- `buildRequestHeaders()` returns a new object each time with `Accept` and, when a token exists, `Authorization`.
- That object is passed straight into axios for PUT. There is no step that removes or overwrites `Authorization` when the body is FormData.
- So **in the frontend**, FormData does **not** cause the Authorization header to be lost.

**Backend / environment:** The only way the header could be “lost” would be outside the dashboard code: e.g. a proxy, Apache (e.g. stripping `Authorization` in some cases), or **CORS preflight** (see Step 5). The backend’s `.htaccess` re-injects `Authorization` from `%{HTTP:Authorization}` for all requests that hit the front controller; that applies to both GET and PUT.

**Conclusion:** Using FormData does **not** override or strip the Authorization header in the frontend. Headers are built once and passed through to axios unchanged for Authorization.

---

## STEP 3 — Compare with a working endpoint (e.g. GET /companies/{id})

### 3.1 Request creation and headers

| Aspect | GET /companies/{id} (getCompanyById) | PUT /companies/{id} (editCompany) |
|--------|--------------------------------------|------------------------------------|
| **Http instance** | `this.http` = `Http.getInstance()` | Same |
| **Method** | `this.http.get(COMPANY_BASE/${company_id})` | `this.http.put(COMPANY_BASE/${id}, company)` |
| **Implementation** | `HttpImplementation.get()` | `HttpImplementation.put()` |
| **buildRequestHeaders()** | Called at start of `get()` | Called at start of `put()` |
| **Headers sent** | `headers` from buildRequestHeaders() (Accept + Authorization if token) | Same |
| **Body** | None | FormData |
| **Base URL** | `COMPANY_BASE` = `${environments.API_PUBLIC_URL}/companies` | Same |

So the **only** differences are HTTP method (GET vs PUT) and the presence of a FormData body. The way the token is read from the cookie and added to headers is **identical**.

### 3.2 Backend route and middleware

- Both **GET** `api/companies/{company}` (show) and **PUT** `api/companies/{company}` (update) are registered in the **same** middleware group in `routes/api.php` (lines 87–111):
  - `auth:sanctum`, `verified`, `nomade.verified`, `LogCompanyUpdateRoute`.
- So both require the same authentication. If GET succeeds, the token is valid and accepted by the backend for that route group.

**Conclusion:** The request creation and auth header handling are the same for GET and PUT. The backend protects both with the same middleware. So the reason “only PUT fails” is **not** a different code path or different middleware for the PUT route.

---

## STEP 4 — Base URL check

### 4.1 API_PUBLIC_URL used in this request

- `CompanyRepository` uses `COMPANY_BASE` from `@company/application/routes`.
- `routes.ts` defines: `COMPANY_BASE = `${environments.API_PUBLIC_URL}/companies``.
- `environments.API_PUBLIC_URL` comes from `process.env.VITE_PUBLIC_API_URL` (environments.ts).

So the PUT URL is:

- **`${environments.API_PUBLIC_URL}/companies/${id}`**

(e.g. `https://api.example.com/api/companies/123` if `VITE_PUBLIC_API_URL` is `https://api.example.com/api`).

### 4.2 Is it exactly the same as other endpoints?

**Yes.**

- `getCompanyById` uses `${COMPANY_BASE}/${company_id}` → same base.
- `deleteCompany` uses `${COMPANY_BASE}/${company_id}` → same base.
- `editCompany` uses `${COMPANY_BASE}/${id}` → same base.
- All use the same `COMPANY_BASE` and thus the same `API_PUBLIC_URL`.

**Conclusion:** The base URL and env used for PUT are the same as for GET and other company endpoints. So 401 is not caused by a different host or path.

---

## STEP 5 — Root cause: why ONLY this endpoint fails; missing vs invalid header

### 5.1 What we know

- **Frontend:** PUT uses the same Http instance, the same `buildRequestHeaders()`, and the same base URL as GET. FormData does not strip or override the Authorization header in code.
- **Backend:** PUT and GET live in the same route group and middleware; 401 is returned when **auth:sanctum** fails (no valid auth).
- **Observation:** Other authenticated endpoints (e.g. GET /companies/{id}) work, so the token is present in the cookie and is valid for the backend when it is sent.

So the only plausible explanation for “only PUT returns 401” is that the **PUT request**, in practice, is **not** sending the `Authorization` header (or the backend is not seeing it), while GET and others **are** sending it.

### 5.2 Most likely cause: CORS preflight and Authorization

- **GET** to the same origin/API is often a **“simple”** request (simple method, simple headers). Browsers may **not** send an OPTIONS preflight; they send GET with `Authorization` directly.
- **PUT** with a non-simple body (e.g. FormData → `Content-Type: multipart/form-data`) or simply because it is PUT is **not** simple, so the browser sends an **OPTIONS preflight** first.
- For the preflight, the server must respond with **Access-Control-Allow-Headers** including **Authorization**. If the server does **not** list `Authorization` in that CORS response, the browser will **not** attach the `Authorization` header to the **actual** PUT request (to protect the user from non-CORS-aware servers). So the PUT would hit the API **without** the Bearer token → **401**.
- GET (no preflight or a preflight that already allows Authorization) would still send the token and succeed.

So:

- **Authorization is effectively missing** on the PUT request when it reaches the server, because the **browser** does not send it due to CORS rules.
- The token is still valid and present in the cookie; the frontend still calls `buildRequestHeaders()` and would add it to the axios config. So the “invalid token” path (same header, wrong/expired token) is **less** likely than “header not sent on this specific request type.”

### 5.3 How to confirm

1. **Browser DevTools → Network:**  
   - Select the **failing PUT** to `.../companies/{id}`.  
   - Check the **Request Headers**: is `Authorization: Bearer ...` present?  
   - If **no** → supports CORS/preflight (or similar) preventing the header from being sent.  
   - If **yes** → then the problem is on the server (e.g. not reading the header for this request).

2. **Preflight:**  
   - Check for an **OPTIONS** request to the same URL just before the PUT.  
   - Inspect the **Response Headers**: does **Access-Control-Allow-Headers** include **Authorization** (or a wildcard that covers it)?  
   - If it does **not** include Authorization → that explains why only PUT (and any other non-simple request) fails with 401.

3. **Backend auth log:**  
   - The `Authenticate` middleware logs `has_auth_header` and `has_cookie`.  
   - For the failing PUT, if the log shows `has_auth_header => false`, the server is not receiving the header, consistent with the browser omitting it (e.g. due to CORS).

### 5.4 Other possible (less likely) causes

- **Cookie/token only for GET:** Theoretically, if the token were only sent or only valid in a specific context (e.g. only for “simple” requests), only GET could succeed. That would still point to “Authorization missing on PUT” rather than “invalid token.”
- **Proxy or gateway:** A reverse proxy might strip the `Authorization` header for certain methods or content types. That would also lead to “missing” on the server for PUT only.
- **Apache:** The `.htaccess` rule re-injecting `Authorization` might behave differently for PUT in some setups (e.g. if the request is handled differently). Less common but possible; checking backend logs for `has_auth_header` would tell.

### 5.5 Summary

- **Why only this endpoint fails:** Because only this request (PUT with FormData) triggers a CORS preflight and/or is treated as non-simple, so the browser may omit the `Authorization` header if the server’s CORS config does not allow it. GET and other simple requests keep sending the header and succeed.
- **Missing vs invalid:** The evidence points to the **Authorization header being missing** on the PUT request when it reaches the server (due to CORS or similar), not to an invalid or expired token. The same token works for GET and other endpoints when the header is sent.

**No code changes were made; this document is analysis only.**
