Skip to content

Commit

Permalink
fix: state management for raw http requests (promptfoo#2491)
Browse files Browse the repository at this point in the history
  • Loading branch information
typpo authored Dec 21, 2024
1 parent 2ea2292 commit 75e67ed
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 121 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useCallback, useState } from 'react';
import React, { useCallback, useState } from 'react';
import Editor from 'react-simple-code-editor';
import AddIcon from '@mui/icons-material/Add';
import CheckIcon from '@mui/icons-material/Check';
Expand Down Expand Up @@ -38,8 +38,6 @@ interface HttpEndpointConfigurationProps {
setBodyError: (error: string | null) => void;
urlError: string | null;
setUrlError: (error: string | null) => void;
forceStructured?: boolean;
setForceStructured: (force: boolean) => void;
updateFullTarget: (target: ProviderOptions) => void;
}

Expand All @@ -51,7 +49,9 @@ interface GeneratedConfig {
headers?: Record<string, string>;
body?: any;
request?: string;
transformRequest?: string;
transformResponse?: string;
sessionParser?: string;
};
}

Expand All @@ -62,18 +62,11 @@ const HttpEndpointConfiguration: React.FC<HttpEndpointConfigurationProps> = ({
setBodyError,
urlError,
setUrlError,
forceStructured,
setForceStructured,
updateFullTarget,
}): JSX.Element => {
const theme = useTheme();
const darkMode = theme.palette.mode === 'dark';

// Internal state management
const [useRawRequest, setUseRawRequest] = useState(
forceStructured ? false : !!selectedTarget.config.request,
);
const [rawRequestValue, setRawRequestValue] = useState(selectedTarget.config.request || '');
const [requestBody, setRequestBody] = useState(
typeof selectedTarget.config.body === 'string'
? selectedTarget.config.body
Expand Down Expand Up @@ -122,7 +115,6 @@ Content-Type: application/json

if (isRawMode) {
// Reset to empty raw request
setRawRequestValue('');
updateCustomTarget('request', '');

// Clear structured mode fields
Expand All @@ -132,7 +124,6 @@ Content-Type: application/json
updateCustomTarget('body', undefined);
} else {
// Reset to empty structured fields
setRawRequestValue('');
setHeaders([]);
setRequestBody('');

Expand Down Expand Up @@ -223,59 +214,17 @@ Content-Type: application/json
[headers, updateCustomTarget],
);

const handleRequestBodyChange = (code: string) => {
setRequestBody(code);
// Update state immediately without validation
updateCustomTarget('body', code);
const handleRequestBodyChange = (content: string) => {
setRequestBody(content);
updateCustomTarget('body', content);
};

const handleRawRequestChange = (value: string) => {
setRawRequestValue(value);
// Update state immediately without validation
updateCustomTarget('request', value);
};

// Separate validation from state updates
useEffect(() => {
if (!useRawRequest) {
setBodyError(null);
return;
}

// Don't show errors while typing short content
if (!rawRequestValue.trim() || rawRequestValue.trim().length < 20) {
setBodyError(null);
return;
}

// Debounce validation to avoid blocking input
const timeoutId = setTimeout(() => {
const request = rawRequestValue.trim();

// Check for required template variable
if (!request.includes('{{prompt}}')) {
setBodyError('Request must include {{prompt}} template variable');
return;
}

// Check for basic HTTP request format
const firstLine = request.split('\n')[0];
const hasValidFirstLine = /^(POST|GET|PUT|DELETE)\s+\S+/.test(firstLine);
if (!hasValidFirstLine) {
setBodyError('First line must be in format: METHOD URL');
return;
}

setBodyError(null);
}, 750);

return () => clearTimeout(timeoutId);
}, [useRawRequest, rawRequestValue]);

// Note to Michael: don't dedent this, we want to preserve JSON formatting.
const placeholderText = `Enter your HTTP request here. Example:
POST /v1/chat/completions HTTP/1.1
const exampleRequest = `POST /v1/chat/completions HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer {{api_key}}
Expand All @@ -288,35 +237,9 @@ Authorization: Bearer {{api_key}}
}
]
}`;
const placeholderText = `Enter your HTTP request here. Example:
// Add effect to handle forceStructured changes
useEffect(() => {
if (forceStructured) {
// Reset all state to structured mode
setUseRawRequest(false);
setRawRequestValue('');
// Clear any validation errors
setUrlError(null);
setBodyError(null);

setHeaders(
Object.entries(selectedTarget.config.headers || {}).map(([key, value]) => ({
key,
value: String(value),
})),
);
setRequestBody(
typeof selectedTarget.config.body === 'string'
? selectedTarget.config.body
: JSON.stringify(selectedTarget.config.body, null, 2) || '',
);

// Only update the target config if we need to clear the raw request
if (selectedTarget.config.request) {
updateCustomTarget('request', undefined);
}
}
}, [forceStructured, selectedTarget.config]);
${exampleRequest}`;

const handleGenerateConfig = async () => {
setGenerating(true);
Expand Down Expand Up @@ -355,11 +278,10 @@ Authorization: Bearer {{api_key}}
const handleApply = () => {
if (generatedConfig) {
if (generatedConfig.config.request) {
setUseRawRequest(true);
setRawRequestValue(generatedConfig.config.request);
resetState(true);
updateCustomTarget('request', generatedConfig.config.request);
} else {
setUseRawRequest(false);
resetState(false);
if (generatedConfig.config.url) {
updateCustomTarget('url', generatedConfig.config.url);
}
Expand Down Expand Up @@ -387,9 +309,9 @@ Authorization: Bearer {{api_key}}
updateCustomTarget('body', generatedConfig.config.body);
}
}
if (generatedConfig.config.transformResponse) {
updateCustomTarget('transformResponse', generatedConfig.config.transformResponse);
}
updateCustomTarget('transformRequest', generatedConfig.config.transformRequest);
updateCustomTarget('transformResponse', generatedConfig.config.transformResponse);
updateCustomTarget('sessionParser', generatedConfig.config.sessionParser);
setConfigDialogOpen(false);
}
};
Expand All @@ -407,21 +329,23 @@ Authorization: Bearer {{api_key}}
<FormControlLabel
control={
<Switch
checked={useRawRequest}
checked={!!selectedTarget.config.request}
onChange={(e) => {
const enabled = e.target.checked;
resetState(enabled);
setUseRawRequest(enabled);
if (enabled) {
updateCustomTarget('request', exampleRequest);
}
}}
/>
}
label="Use Raw HTTP Request"
sx={{ mb: 2, display: 'block' }}
/>
{useRawRequest ? (
{selectedTarget.config.request ? (
<Box mt={2} p={2} border={1} borderColor="grey.300" borderRadius={1}>
<Editor
value={rawRequestValue}
value={selectedTarget.config.request}
onValueChange={handleRawRequestChange}
highlight={(code) => highlight(code, languages.http)}
padding={10}
Expand Down
31 changes: 6 additions & 25 deletions src/app/src/pages/redteam/setup/components/Targets/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,6 @@ export default function Targets({ onNext, onBack, setupModalOpen }: TargetsProps
const [missingFields, setMissingFields] = useState<string[]>([]);
const [promptRequired, setPromptRequired] = useState(requiresPrompt(selectedTarget));
const [testingEnabled, setTestingEnabled] = useState(selectedTarget.id === 'http');
const [forceStructuredHttp, setForceStructuredHttp] = useState(false);

const { recordEvent } = useTelemetry();
const [rawConfigJson, setRawConfigJson] = useState<string>(
Expand Down Expand Up @@ -132,7 +131,6 @@ export default function Targets({ onNext, onBack, setupModalOpen }: TargetsProps
}, [selectedTarget, updateConfig]);

const handleTargetChange = (event: SelectChangeEvent<string>) => {
setForceStructuredHttp(false);
const value = event.target.value as string;
const currentLabel = selectedTarget.label;
recordEvent('feature_used', { feature: 'redteam_config_target_changed', target: value });
Expand Down Expand Up @@ -217,30 +215,15 @@ export default function Targets({ onNext, onBack, setupModalOpen }: TargetsProps
const bodyStr = typeof value === 'object' ? JSON.stringify(value) : String(value);
if (bodyStr.includes('{{prompt}}')) {
setBodyError(null);
} else {
} else if (!updatedTarget.config.request) {
setBodyError('Request body must contain {{prompt}}');
}
} else if (field === 'request') {
try {
const requestStr = String(value).trim();
if (requestStr.includes('{{prompt}}')) {
updatedTarget.config.request = requestStr;
delete updatedTarget.config.url;
delete updatedTarget.config.method;
delete updatedTarget.config.headers;
delete updatedTarget.config.body;
setBodyError(null);
} else {
setBodyError('Request must contain {{prompt}} template variable');
return;
}
} catch (err) {
const errorMessage = String(err)
.replace(/^Error:\s*/, '')
.replace(/\bat\b.*$/, '')
.trim();
setBodyError(`Invalid HTTP request format: ${errorMessage}`);
return;
updatedTarget.config.request = value;
if (value && !value.includes('{{prompt}}')) {
setBodyError('Raw request must contain {{prompt}} template variable');
} else {
setBodyError(null);
}
} else if (field === 'label') {
updatedTarget.label = value;
Expand Down Expand Up @@ -443,8 +426,6 @@ export default function Targets({ onNext, onBack, setupModalOpen }: TargetsProps
setBodyError={setBodyError}
urlError={urlError}
setUrlError={setUrlError}
forceStructured={forceStructuredHttp}
setForceStructured={setForceStructuredHttp}
updateFullTarget={setSelectedTarget}
/>
)}
Expand Down

0 comments on commit 75e67ed

Please sign in to comment.